sport 659224 1280

C++ Core Guidelines: Improved Performance with Iostreams

As easy as my title and the rules of the C++ core guidelines sound, getting more performance out of the iostreams is no no-brainer.

 

sport 659224 1280

Okay, let’s step back. Although I did a lot of tests,  my numbers in this post are more controversial than I thought. Please let me know if you have any ideas, improvements, or clarifications, and I will add them to this post.

Here are the two performance-related rules from the guidelines to Iostreams.

I assume you don’t know std::ios_base::sync_with_stdio?

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    SL.io.10: Unless you use printf-family functions call ios_base::sync_with_stdio(false)

    Per default, operations on the C++ streams are synchronized with the C streams. This synchronization happens after each in- or output operation.

    This allows it to mix C++ and C in- or output operations because operations on the C++ streams go unbuffered to the C streams. What is also important to note from the concurrency perspective: synchronized C++ streams are thread-safe. All threads can write to the C++ streams without any need for synchronization. The effect may be an interleaving of characters but not a data race.

    When you set the std::ios_base::sync_with_stdio(false), the synchronization between C++ streams and C streams will not happen because the C++ stream may put its output into a buffer. Because of the buffering, the in- and output operation may become faster. You must invoke std::ios_base::sync_with_stdio(false) before any in- or output operation. If not, the behavior is implementation-defined.

    I assume you noticed that I wrote quite often, maybe. That is for a reason.

    Interleaving of C++ Streams and C Streams

    First,  I want to know what would happen when I execute the following program with various compilers.

    // syncWithStdio.cpp
    
    #include <iostream>
    #include <cstdio>
     
    int main(){
        
        std::ios::sync_with_stdio(false);
    
        std::cout << std::endl;
        
        std::cout << "1";
        std::printf("2");
        std::cout << "3";
        
        std::cout << std::endl;
        
    }
    

     

    I add a few pieces of information to get a better picture of my various compiler.

    GCC 8.2

     gcc

    SyncWithStdioLinux

    Clang 8.0

    clang

    SyncWithStdioClang

     

    cl.exe 19.20

     clexe

     

     SyncWithStdioWin

     

    It seems that only the output on GCC is not synchronized. This observation does not hold for clang or cl.exe on Windows. A small performance test confirmed my first impression.

    Performance with and without Synchronisation

    Let me write a small program with and without synchronization to the console. Doing it without synchronization should be faster.

    • Synchronized

     

    // syncWithStdioPerformanceSync.cpp
    
    #include <chrono>
    #include <fstream>
    #include <iostream>
    #include <random>
    #include <sstream>
    #include <string>
    
    constexpr int iterations = 10;
    
    std::ifstream openFile(const std::string& myFile){                  
    
      std::ifstream file(myFile, std::ios::in);
      if ( !file ){
        std::cerr << "Can't open file "+ myFile + "!" << std::endl;
        exit(EXIT_FAILURE);
      }
      return file;
      
    }
    
    std::string readFile(std::ifstream file){                        
        
        std::stringstream buffer;
        buffer << file.rdbuf();
        
        return buffer.str();
        
    }
    
    auto writeToConsole(const std::string& fileContent){
         
        auto start = std::chrono::steady_clock::now();
        for (auto c: fileContent) std::cout << c;
        std::chrono::duration<double> dur = std::chrono::steady_clock::now() - start;
        return dur;
    }  
    
    template <typename Function>
    auto measureTime(std::size_t iter, Function&& f){
        std::chrono::duration<double> dur{};
        for (int i = 0; i < iter; ++i){
            dur += f();
        }
        return dur / iter;
    }
        
    int main(int argc, char* argv[]){
        
        std::cout << std::endl;
      
        // get the filename
        std::string myFile;
        if ( argc == 2 ){
            myFile= argv[1];
        }
        else{
            std::cerr << "Filename missing !" << std::endl;
            exit(EXIT_FAILURE);
        } 
      
        std::ifstream file = openFile(myFile);                                  // (1)
      
        std::string fileContent = readFile(std::move(file));                    // (2)
    // (3) auto averageWithSync = measureTime(iterations, [&fileContent]{ return writeToConsole(fileContent); }); std::cout << std::endl; // (4) std::cout << "With Synchronisation: " << averageWithSync.count() << " seconds" << std::endl; std::cout << std::endl; }

     

    The program is relatively easy to explain. I open a file (line 1), read its entire content (line 2) into a string, and write its iterations-times to the console (line 3). This is done in the function writeToConsole(fileContent).

    iterations are in my concrete case 10. In the end, I display the average time of the output operations (line 4).

    • Non-synchronized

    The non-synchronized version of the program is quite similar to the synchronized version. Only the main function changed a bit.

    // syncWithStdioPerformanceWithoutSync.cpp
    
    ... 
     
    int main(int argc, char* argv[]){
        
        std::ios::sync_with_stdio(false);    // (1)
    
        std::cout << std::endl;
      
        // get the filename
        std::string myFile;
        if ( argc == 2 ){
            myFile= argv[1];
        }
        else{
            std::cerr << "Filename missing !" << std::endl;
            exit(EXIT_FAILURE);
        } 
      
        std::ifstream file = openFile(myFile);
      
        std::string fileContent = readFile(std::move(file));
        
        auto averageWithSync = measureTime(iterations, [&fileContent]{ return writeToConsole(fileContent); });
        
        auto averageWithoutSync = measureTime(iterations, [&fileContent]{ return writeToConsole(fileContent); });
        
        std::cout << std::endl;
        
        std::cout << "Without Synchronisation: " << averageWithoutSync.count() << " seconds" << std::endl;  
      
        std::cout << std::endl;
        
    }
    

     

    I just added line (1) to the main program. Now, I hope for performance improvement.

    I did my performance test with a small program and a bigger text file (600.000 characters). The bigger file gave me no new insight; therefore, I skipped it.

    >> syncWithStdioPerformanceSync syncWithStdioPerformanceSync.cpp
    >> syncWithStdioPerformanceWithoutSync syncWithStdioPerformanceSync.cpp
    

    GCC

    syncWithStdioPerformanceCppGcc

    Clang

    syncWithStdioPerformanceCppClang

    cl.exe

    syncWithStdioPerformanceCppWin

     

    The results puzzled me because of Windows.

    • With GCC, I had a performance improvement of about 70% in the non-synchronized variant.
    • Neither Clang nor cl.exe showed any performance improvement. It seems that the non-synchronized in- and output operations are synchronized. My numbers proved my observation from the program syncWithStdio.cpp.
    • Only for the record. Did you notice how slow the console on windows is?

    Of course, I’m guilty. I almost always break the following rule.

    SL.io.50: Avoid endl

    Why should you avoid std::endl? Or, to say it differently: What is the difference between the manipulator std::endl and ‘\n’.

    • std::endl: writes a new line and flushes the output buffer.
    • ‘\n’: writes a new line.

    Flushing the buffer is expensive and should, therefore, be avoided. If necessary, the buffer is automatically flushed. Honestly, I was curious to see the numbers. To make it extremely worse, here is my program, which puts a linebreak (line  3) after each character.

    // syncWithStdioPerformanceEndl.cpp
    
    #include <chrono>
    #include <fstream>
    #include <iostream>
    #include <random>
    #include <sstream>
    #include <string>
    
    constexpr int iterations = 500;                                                    // (1)
    
    std::ifstream openFile(const std::string& myFile){                  
    
      std::ifstream file(myFile, std::ios::in);
      if ( !file ){
        std::cerr << "Can't open file "+ myFile + "!" << std::endl;
        exit(EXIT_FAILURE);
      }
      return file;
      
    }
    
    std::string readFile(std::ifstream file){                        
        
        std::stringstream buffer;
        buffer << file.rdbuf();
        
        return buffer.str();
        
    }
    
    template <typename End>
    auto writeToConsole(const std::string& fileContent, End end){
         
        auto start = std::chrono::steady_clock::now();
        for (auto c: fileContent) std::cout << c << end;                                 // (3)
        std::chrono::duration<double> dur = std::chrono::steady_clock::now() - start;
        return dur;
    }  
    
    template <typename Function>
    auto measureTime(std::size_t iter, Function&& f){
        std::chrono::duration<double> dur{};
        for (int i = 0; i < iter; ++i){
            dur += f();
        }
        return dur / iter;
    }
        
    int main(int argc, char* argv[]){
    
        std::cout << std::endl;
      
        // get the filename
        std::string myFile;
        if ( argc == 2 ){
            myFile= argv[1];
        }
        else{
            std::cerr << "Filename missing !" << std::endl;
            exit(EXIT_FAILURE);
        } 
      
        std::ifstream file = openFile(myFile);
      
        std::string fileContent = readFile(std::move(file));
        
        auto averageWithFlush = measureTime(iterations, 
                                            [&fileContent]{ return writeToConsole(fileContent, std::endl<char, std::char_traits<char>>); }); // (2)
        auto averageWithoutFlush = measureTime(iterations, [&fileContent]{ return writeToConsole(fileContent, '\n'); });                     // (3)
        
        std::cout << std::endl;
        std::cout << "With flush(std::endl) " << averageWithFlush.count() << " seconds" << std::endl;  
        std::cout << "Without flush(\\n): " << averageWithoutFlush.count() << " seconds" << std::endl;  
        std::cout << "With Flush/Without Flush: " << averageWithFlush/averageWithoutFlush << std::endl;
        
        std::cout << std::endl;
        
    }
    

     

    In the first case, I did it with std::endl (line 2); in the second case, I did it with ‘\n‘ (line 3). The program is quite similar to the previous one. The big difference is that I made 500 iterations (line 3). Why? I was astonished by the variations in the numbers. With a few iterations, I could not notice any difference. Sometimes, std::endl was two times faster than ‘\n’; sometimes, std::endl was four times slower. I got similar behavior with cl.exe or with GCC.  I also did it with another GCC or cl.exe compiler. Honestly, this was not what I expected. When I did it with 500 iterations, I got the expected winner. ‘\n‘ seems to be 10% – 20% faster than std::endl. Once more, only 10% – 20% faster.

    GCC

    syncWithStdioPerformanceCppLinuxEndl

    cl.exe

     syncWithStdioPerformanceCppWinEndl

     

    My Small Conclusion

    I want to draw a small conclusion from my performance test.

    • std::ios_base::sync_with_stdio(false) can make a big difference on your platform, but you lose your thread-safety guarantee.
    • std::endl is not as bad as its reputation. I will not change my habit.

    What’s next?

    Only one rule exists for the sections regex, chrono, and the C standard library. You see, I have to improvise in my next post.

     

     

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *