schnellere alternative zu std::stringstream?



  • Hallo Leute,

    gibt es eine schnellere alternative zu std::stringstream? Kurzfassung: ich möchte ähnlich wie bei streams (z.B. cout) Zeichenketten mittels '<<' verbinden und zwar möglichst schnell.

    Hier erstmal folgendes Beispiel um mein Problem zu schildern:

    #include <iostream>
    #include <string.h>
    #include <fstream>
    #include <sstream>
    
    using namespace std;
    
    // Quick-and-Dirty-Lösung
    #define DEBUG_OUT(msg)\
    {\
    	std::stringstream ss; \
    	ss << __FILE__ << ": " << msg << "\n"; \
    	strToFile(ss.str());\
    }
    
    void strToFile(std::string const& str)
    {
    	ofstream file;
    
    	file.open ("./myFile.txt", ios::out | ios::app);
    	if (file.is_open())
    	{
    		file << str;
    		file.flush();
    		file.close();
    	}
    }
    
    int main()
    {
    	int value1 = 123;
    	int value2 = 456;
    
    	DEBUG_OUT("simple text");
    	DEBUG_OUT("value 1: " << value1 << "  value 2: " << value2);
    
    	return 0;
    }
    

    Ich habe eine Anwendung die einige zeitkritische Routinen beinhaltet. Das bedeutet die Verarbeitungen sollten möglichst schnell (am besten <50µs) vonstattengehen. Nun sollen einige Debugausgaben getätigt werden um das Verhalten zu beobachten. In den Methoden habe ich dazu temporär ein Makro DEBUG_OUT() in Verwendung. Die übergebenen Werte können dabei wie bei einem Stream mit '<<' verbunden werden. Im Makro werden dann die übergebenen Inhalte in einen std::stringstream gepackt, ggf. ergänzt und dann weitergeleitet. Bei der 'Weiterleitung' schreibe ich die Werte z.B. in die Konsole oder wie im Beispiel in eine Datei.

    Die Ausgabe der Datei sieht im Beispiel dann so aus:

    ../src/test.cpp: simple text
    ../src/test.cpp: value 1: 123  value 2: 456
    

    Zunächst einmal... das Ausgeben der Werte in die Konsole ist zu langsam, daher leite ich die Werte in der Regel in eine Datei. Im Beispiel ist es auch sehr einfach gehalten, da ich normalerweise nicht jedes mal die Datei öffne, flushe und schließe (kostet zu viel Zeit). Hier habe ich es etwas einfach gehalten. Mein Problem liegt im Makro. Allein die Erstellung des stringstream lauert wesentlich länger als ein std::string. Kann ich das irgendwie beschleunigen?

    Ich habe mal geschaut, wieviel Zeit so benötigt wird, um 1 Mio. Objekte zu erstellen oder zu befüllen:

    #include <iostream>
    #include <string.h>
    #include <sstream>
    #include <chrono>
    
    using namespace std;
    
    int main()
    {
    	// Test 1: create std::string
    	auto start_time = std::chrono::high_resolution_clock::now();
    	for (int j = 1; j <= 1000000; j++)
    	{
    		std::string test;
    	}
    	auto end_time = std::chrono::high_resolution_clock::now();
    	std::chrono::duration<double, std::milli>  proccess_time_ms = end_time - start_time;
    	cout.precision(5);
    	std::cout << "time (create std::string): \t\t" << proccess_time_ms.count() << " ms \n";
    
    	// Test 2: create std::stringstream
    	start_time = std::chrono::high_resolution_clock::now();
    	for (int j = 1; j <= 1000000; j++)
    	{
    		std::stringstream test;
    	}
    	end_time = std::chrono::high_resolution_clock::now();
    	proccess_time_ms = end_time - start_time;
    	std::cout << "time (create std::stringstream): \t" << proccess_time_ms.count() << " ms \n";
    
    	// Test 3: concatenate with string
    	start_time = std::chrono::high_resolution_clock::now();
    	for (int j = 1; j <= 1000000; j++)
    	{
    		std::string test;
    		test = "value 1: ";
    		test += ::to_string(3);
    		test += "  value 2: ";
    		test += ::to_string(12.34);
    	}
    	end_time = std::chrono::high_resolution_clock::now();
    	proccess_time_ms = end_time - start_time;
    	std::cout << "time (concatenate with string): \t" << proccess_time_ms.count() << " ms \n";
    
    	// Test 4: concatenate with stringstream
    	start_time = std::chrono::high_resolution_clock::now();
    	for (int j = 1; j <= 1000000; j++)
    	{
    		std::stringstream test;
    		test << "value 1: ";
    		test << 3;
    		test << "  value 2: ";
    		test << 12.34;
    	}
    	end_time = std::chrono::high_resolution_clock::now();
    	proccess_time_ms = end_time - start_time;
    	std::cout << "time (concatenate with stringstream): \t" << proccess_time_ms.count() << " ms \n";
    
    	return 0;
    }
    

    Das kam raus:

    time (create std::string):              5.8863 ms 
    time (create std::stringstream):        504.4 ms 
    time (concatenate with string):         845.61 ms 
    time (concatenate with stringstream):   1071.1 ms
    

    Das Erstellen der stringstreams dauert sehr lange. Allerdings dauert die Erstellung und Befüllung der stringstreams fast so lange wie bei den normalen strings. Das Befüllen an sich ist also schneller als bei den strings. Die Erste Überlegung war, dass ich statt 'std::stringstream' 'std::ostringstream' verwende. Denn werden die Werte zumindest etwas besser:

    time (create std::string):              7.2196 ms 
    time (create std::stringstream):        297.97 ms 
    time (concatenate with string):         880.63 ms 
    time (concatenate with stringstream):   982.3 ms
    

    Oder gibt es andere Möglichkeiten z.B. mit ostream, die schneller sind? Oder sollte ich es irgendwie versuchen über einen anderen Thread zu verarbeiten?
    Kann ich da was machen?

    viele Grüße,
    SBond



  • Zeitkritisches loggen, hmm..
    Ich hätte dir jetzt auch verzögertes Loggen mittels Thread empfohlen. Was du noch versuchen kannst:

    stringstream-Objekt wiederverwenden.
    Manuelles formatieren.
    Nach stringstream-Alternative suchen (sorry, kenne keine, gibt aber sicher was).



  • Was machst du eigentlich, dass du dich um die paar Millisekunden kümmerst?



  • Es geht um kryptografisch gesicherte Zeitsynchronisation von Computersystemen (im Mikrosekundenbereich). Verarbeitungszeiten von Nachrichten sollten im Idealfall <80µs liegen, um Schwankungen und dynamische Fehler vernachlässigen zu können. Wenn die Erstellung und Befüllung eines Objekts allerdings 5-30 µs braucht, dann ist das schon viel für mich. Gerade Debugausgaben kosten richtig Zeit und verfälschen Werte (gehen teils in den Millisekundenbeeich), daher brauche ich irgendwie Ausgaben, die möglichst wenig Ressourchen benötigen. Das Schreiben in eine Datei ist schonmal performanter als Konsolenausgaben, aber die String-operationen sind sehr zeitintensiv.

    @Hi: ...die Idee das stringstream erneut zu verwenden hat schon was. Mal sehen ob ich da was hinkriege.



  • SBond schrieb:

    Es geht um kryptografisch gesicherte Zeitsynchronisation von Computersystemen (im Mikrosekundenbereich). Verarbeitungszeiten von Nachrichten sollten im Idealfall <80µs liegen, um Schwankungen und dynamische Fehler vernachlässigen zu können. Wenn die Erstellung und Befüllung eines Objekts allerdings 5-30 µs braucht, dann ist das schon viel für mich. Gerade Debugausgaben kosten richtig Zeit und verfälschen Werte (gehen teils in den Millisekundenbeeich), daher brauche ich irgendwie Ausgaben, die möglichst wenig Ressourchen benötigen. Das Schreiben in eine Datei ist schonmal performanter als Konsolenausgaben, aber die String-operationen sind sehr zeitintensiv.

    @Hi: ...die Idee das stringstream erneut zu verwenden hat schon was. Mal sehen ob ich da was hinkriege.

    Debug ausgaben und high performance (= geringe latenz) wiedersprechen sich meiner Meinung nach.
    Was bei einem std::string helfen könnte, wenn der string mit std::string::reserve
    groß genug zu machen das beim zusammenfügen von strings keine reallocation notwendig wird.



  • SBond schrieb:

    Zeitsynchronisation von Computersystemen

    also brauchst du auch nur
    << (str zu compile-zeit)
    und
    << zahl

    ja?



  • ja das stimmt. Performance und Debugausgaben sind gegenläufig. Im normalen Betrieb werden auch keine Debugausgaben ausgegeben. Für einfache Tests und Vorführungen ist es aber schön zu sehen, wenn aktiv etwas passiert. Bringt leider nur nichts, wenn die Performance so weit sinkt, dass das Programm zu ungenau wird.

    unskilled schrieb:

    SBond schrieb:

    Zeitsynchronisation von Computersystemen

    also brauchst du auch nur
    << (str zu compile-zeit)
    und
    << zahl

    ja?

    Jain. in der Regel schon, aber die Ausgaben könnten umfangreicher sein.



  • ich hatte mir so was in etwa gedacht.
    habs auch kurz getestet weil elende fricklig, aber bin mir noch nicht zu 100% sicher, dass fehlerfrei^^

    #include <string>
    #include <ostream>
    #include <cassert>
    
    namespace detail { struct debug_proxy; }
    
    void __declspec(noinline) WRITE_TO_HDD(const char* i, unsigned l)
    {
    	i;
    	l;
    }
    
    struct debug_out_t
    {
    	debug_out_t() : size(0) {}
    
    	template< unsigned LEN >
    	detail::debug_proxy operator<<(const char(&in)[LEN])
    	{
    		assert( size == 0 );
    
    			            //data + number + sign + \n
    		data = new char[LEN + std::numeric_limits<long long>::digits10 + 1 + 1];
    
    		for( size = 0; size != LEN-1; ++size)
    			data[size] = in[size];
    
    		return detail::debug_proxy(*this);
    	}
    
    	debug_out_t& operator<<(std::ostream&(*manip)(std::ostream&))
    	{
    		if( manip == std::endl )
    		{
    			data[size++] = '\n';
    			flush();
    		}
    
    		return *this;
    	}
    
    	~debug_out_t()
    	{
    		try
    		{
    			flush();
    		}
    		catch (...)
    		{
    		}
    	}
    
    	void close()
    	{
    		if( size == 0 )
    			return;
    
    		size = 0;
    		delete[] data;
    	}
    private:
    	debug_out_t( const debug_out_t& ) = delete;
    
    	void flush()
    	{
    		WRITE_TO_HDD(data, size);
    
    		close();
    	}
    
    	friend detail::debug_proxy;
    
    	char* data;
    	unsigned size;
    };
    
    namespace detail
    {
    	struct debug_proxy
    	{
    		debug_proxy(debug_out_t& s) : s(s) {}
    
    		template< class INT_TYPE>
    		typename std::enable_if< std::is_integral<INT_TYPE>::value, debug_out_t&>::type
    			operator<<(INT_TYPE val)
    		{
    			bool neg = val < 0;
    			if( neg )
    				s.data[s.size++] = '-';
    
    			unsigned max_len = std::numeric_limits<INT_TYPE>::digits10;
    
    			for( unsigned pos = max_len ; pos != 0; --pos)
    			{
    				s.data[s.size+pos-1] = val%10 + '0';
    				val /= 10;
    			}
    
    			s.size += max_len;
    
    			return s;
    		}
    
    	private:
    		debug_out_t& s;
    	};
    }
    
    #include <iostream>
    #include <sstream>
    #include <chrono>
    
    using namespace std;
    
    #include <random>
    
    int main()
    {
    	std::random_device rdm;
    
    	// Test 1
    	auto start_time = std::chrono::high_resolution_clock::now();
    	for (int j = 1; j <= 1000000; j++)
    	{
    		std::stringstream test;
    		test << "asd " << rdm() << std::endl;
    		WRITE_TO_HDD( test.str().data(), test.str().size() );
    	}
    	auto end_time = std::chrono::high_resolution_clock::now();
    	std::chrono::duration<double, std::milli>  proccess_time_ms = end_time - start_time;
    	cout.precision(5);
    	std::cout << "time (1): \t\t" << proccess_time_ms.count() << " ms \n";
    
    	// Test 2
    	start_time = std::chrono::high_resolution_clock::now();
    	for (int j = 1; j <= 1000000; j++)
    	{
    		debug_out_t test;
    		test << "asd " << rdm() << std::endl;
    	}
    	end_time = std::chrono::high_resolution_clock::now();
    	proccess_time_ms = end_time - start_time;
    	std::cout << "time (2): \t" << proccess_time_ms.count() << " ms \n";
    
    	char x;
    	std::cin >> x;
    }
    

    time (1): 1093.6 ms
    time (2): 128.3 ms

    vll ja zumindest ein brauchbarer ansatz?! 🙂

    bb



  • cool. Dankeschön für die Hilfe. Ich werde es mir morgen mal genauer ansehen. Kann es gerade noch nicht kompilieren, da diverse Fehler angezeigt werden ('noinline' nicht definiert und so...). Mit was kompilierst du? Ich nutze g++ 4.8 unter Linux Debian




Log in to reply