Experimental: Coroutine-Allokation spezifizieren



  • Hi!

    Ich spiele gerade ein wenig mit den Coroutinen in Visual Studio 2017 und bin recht überrascht, welch ausdrucksstarken, komplett asynchronen Code man produzieren kann, hier anhand eines einfachen TCP-Servers mit HTTP-"Echo":

    #include <...>
    
    void worker(Instance&, Queue&, Socket&);
    
    int main() 
    try {
    	Instance inst{ };
    	Queue q;
    	Socket s{ q };
    	s.bind({ "http", true });
    	s.listen();
    
    	Threadpool{ [&inst, &q, &s]() { worker(inst, q, s); }, 1 }.join();
    }
    catch(const std::system_error& e) {
    	std::cerr << "Exception (Code: " << e.code().value() << ")!\n\t" << e.what();
    }
    
    task handle(Socket s) {
    	char buf[512]{ };
    	auto transferred = co_await s.async_receive(buf);
    	char answer[] = u8"HTTP/1.1 200 OK\r\nServer: GehtDichNixAn!\r\nContent-type: text/html; charset=utf-8\r\n\r\n"
    		"<!doctype html><html><head><title>It works!</title></head><body><h1>It works!</h1><hr />Well done!</body></html>";
    	co_await s.async_send(answer);
    	s.close();
    }
    
    task server(Queue& q, Socket& l) {
    	for(;;) try {
    		auto task = handle(co_await l.async_accept(q));
    	}
    	catch(std::system_error& e) {
    		std::cerr << e.code().value() << ' ' << e.what();
    	}
    }
    
    void worker(Instance& inst, Queue& q, Socket& l)
    try {
    	server(q, l);
    	q.dequeue();
    }
    catch(...) { }
    

    (Ganzer Code zum Ausprobieren für VS2017/2015 hier.)
    Das ist, was Leserlichkeit angeht, gefühlt tausend mal besser als eine Callback-Hell oder irgendwelche Verrenkungen mit future.then().then().then()...

    Ich will aber auch meinen, dass man einen guten Performance-Vorteil herausholen kann, wenn man es irgendwie schafft, den Coroutine-Frame wieder zu verwenden, nach dem die Coroutine das letzte mal aus handle() zurückkehrt. Jedes mal, wenn handle() aufgerufen wird, findet eine Heap-Allokation für den Frame statt (bloß wo?), doch ich kann theoretisch ja einfach den alten Frame samt Socket-Handle wiederverwenden und mir so die Allokation gänzlich sparen, ohne irgendwelche (threadsicheren) Memory-Pools zu programmieren (!). Das automatische Löschen des Frames kann man deaktivieren (bei final_suspend() suspend_always zurückgeben), aber für die Allokation hab ich keine Idee. Ich kann vermutlich den operator new() vom Promise überladen, aber von dort weiß ich dann immer noch nicht, wie ich die Adresse vom alten Frame zurückgeben kann...



  • Hier genauer was ich meine:

    extern thread_local void* cache;
    
    struct task {
    	struct promise_type : operation {
    		...
    
    		void* operator new(std::size_t count) {
    			if(cache != nullptr) {
    				void* ret = nullptr;
    				std::swap(ret, cache);
    				return ret;
    			}
    			return ::operator new(count);
    		}
    
    		void operator delete(void* ptr) {
    			if(cache == nullptr) cache = ptr;
    			else ::operator delete(ptr);
    		}
    	};
    

    Das funktioniert zwar, aber cached jetzt nur einen einzigen Frame. Kann ich das irgendwie verallgemeinern?

    Edit: So geht's (per intrusivem Stack als vorgeschaltete Free-List):

    extern thread_local std::size_t* cache;
    
    struct task {
    	struct promise_type : operation {
    		task get_return_object() { return { std::experimental::coroutine_handle<promise_type>::from_promise(*this) }; }
    		std::experimental::suspend_never initial_suspend() { return { }; }
    		std::experimental::suspend_never final_suspend() { return { }; }
    
    		void* operator new(std::size_t count) {
    			if(cache != nullptr) {
    				void* ret = cache;
    				cache = reinterpret_cast<std::size_t*>(*cache);
    				return ret;
    			}
    			return ::operator new(count);
    		}
    
    		void operator delete(void* ptr) {
    			*static_cast<std::size_t*>(ptr) = reinterpret_cast<std::size_t>(cache);
    			cache = static_cast<std::size_t*>(ptr);
    		}
    	};
    	...
    };
    

    Das ist jetzt natürlich überhaupt nicht allgemein gültig, sondern kann nur in diesem Programm klappen. Sobald man irgendwelche anderen Coroutinen (mit anderen Stackframes) mit dem gleichen Promise-Typ benutzt, geht's gewaltig schief. Jetzt mache ich mich mal an die Benchmarks mit Callbacks. Trotzdem danke!

    Edit²: Allerdings wäre mir eine andere Lösung immer noch lieber. Jetzt kann es theoretisch passieren, dass ein Thread munter alloziiert und ein anderer den Speicher eine seine List dealloziiert und mir dadurch der Speicher ausgeht. Es wäre viel naheliegender, wenn ich einfach direkt den freien Speicher wiederverwenden kann, ohne ihn erst in eine List zu packen, dann würde er auch nicht zwischen den Threads wandern...


Anmelden zum Antworten