C++ och dess framtid att programmera minnessäkert - Hur går utvecklingen?

Permalänk
Medlem

Tips om någon vill skriva en komplett webserver i C++. Kombinera något unikt (exempelvis en uuid) samt index för listan. Den här kombinationen används för användare. uuid är mest för säkerhet, index för att snabbt hitta användaren.

man kan förallokera, låt säga att en gräns sätts till 10 000 samtida användare

Den lösningen är fantastiskt snabb och det kommer dröja länge innan servern slår i taket och då handlar det troligen om något helt annat än att max antal användare nåtts.

Permalänk
Datavetare
Skrivet av KWARF:

Som din lista visar på så jämför du ju lite äpplen och päron i ditt benchmark när boost tvingas till "traditionell" trådning och Go arbetar med sina lightweight goroutines.

En mer rättvis jämförelse vore att använda async coroutines även i Boost, som det här exemplet (som jag saxade och anpassade från github.com/evilenzo/coroutine-server då jag själv har minimal Boost-erfarenhet):

Klicka för mer information

#include <iostream> #include <thread> #include <boost/asio/as_tuple.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/use_awaitable.hpp> #include <boost/beast/core.hpp> #include <boost/beast/http.hpp> namespace asio = boost::asio; namespace beast = boost::beast; namespace http = beast::http; namespace ip = asio::ip; using tcp = ip::tcp; asio::awaitable<void> handle_request(beast::tcp_stream &stream, http::request<http::string_body> request) { http::response<http::string_body> response(http::status::ok, request.version()); response.set(http::field::content_type, "text/html"); response.body() = "<h1>Hello, World!</h1>"; response.prepare_payload(); co_await http::async_write(stream, response, asio::use_awaitable); } asio::awaitable<void> poll_socket(tcp::socket socket) { http::request<http::string_body> request; beast::tcp_stream stream{std::move(socket)}; beast::flat_buffer buffer; for (;;) { auto [ec, size] = co_await http::async_read( stream, buffer, request, asio::as_tuple(asio::use_awaitable)); if (ec) { if (ec == http::error::end_of_stream) { break; } std::cerr << ec.message() << std::endl; break; } bool close{request.need_eof()}; co_await handle_request(stream, std::move(request)); if (close) { break; } request = {}; } stream.socket().shutdown(tcp::socket::shutdown_send); } asio::awaitable<void> poll_connections(asio::ip::address address, uint16_t port) { auto executor = co_await asio::this_coro::executor; tcp::endpoint endpoint{address, port}; tcp::acceptor acceptor{executor}; acceptor.open(endpoint.protocol()); acceptor.bind(endpoint); acceptor.listen(); tcp::socket socket{executor}; for (;;) { co_await acceptor.async_accept(socket, asio::use_awaitable); asio::co_spawn(executor, poll_socket(std::move(socket)), asio::detached); socket = tcp::socket{executor}; } } int main(int argc, char *argv[]) { const int threads = std::thread::hardware_concurrency(); asio::io_context ioc{threads}; asio::co_spawn(ioc.get_executor(), poll_connections(ip::make_address("0.0.0.0"), 8082), asio::detached); std::vector<std::thread> v(threads - 1); for (int i = 1; i < threads; i++) { v.emplace_back([&ioc] { ioc.run(); }); } ioc.run(); }

Visa mer

Jag får då följande resultat på min Macbook Air:

Ditt Go-exempel

160.82k/s

Ditt Boost-exempel

2.96k/s

async Boost

213.41k/s

Med det sagt skulle även jag välja Go före C++ för webbservrar/applikationer, men det kliade lite för mycket i fingrarna när jag läste att C++ skulle prestera så långsamt. 😅

Danke! Var exakt något likt det där i alla fall jag ville se från @klk.

Och som jag skrev ovan: var min första nätverks-server skriven i boost så förväntande mig inte att den skulle prestera på topp. Minskar man antalet trådar till 2-3 stycken ökar prestanda i den en del, då håller den sig på 1/3 till 1/4-del av async versionen på native Linux 24.04 (som med den versionen presterar långt bättre än MacOS...).

Men om man verkligen ska jämföra äpplen och äpplen finns fortfarande relevanta och andra lite mindre relevanta skillnader.

1. Om du tittar på "Transfers/sec" i wrk kommer du notera att Go-versionen skickar väsentligt mycket mer data trots att det är exakt samma HTML-body. Orsaken är att den alltid lägger till header "Date: " är för att det är ett hårt krav enligt RFC9110.

När det är så lite data i body blir en sådan skillnad högst relevant. Får detta när boost-async versionen och Go versionen producerar exakt samma svar

Go: 94.8k requests/sec, 12.48 MB/s C++: 84.3k requests/sec, 11.09 MB/s

om man kör med 100k samtida användare.

2. Vilket tar oss till det större problemet med boost/async versionen. Async-boost versionen core-dumpar (testade både på x86 Ubuntu 24.04 och ARM64 Ubuntu 24.04 med exakt samma resultat) när testet avslutas om man har fler än 1017 samtida användare. Detta kräver C++20 (som ingen kompilator än fullt ut implementerar) och väldigt ny version av boost, uppenbarligen inte helt stabilt än.

Den versionen jag postade må använda "gammal" teknik (går att kompilera med C++14), men den kraschade i alla fall inte.

3. Ändå helcoolt att stackless co-routines can ge sådan boost i prestanda. Men just då de är stackless så är det egentligen inte en riktigt "äpplen mot äppen" ställd mot Go (som har beteende motsvarande C++ stackful coroutines). Det finns en del extra begränsningar med stackless coroutines, en är att det är rätt svårt att debugga (precis som await/async i C# visade sig vara horribelt att debugga).

Men framförallt har man begränsningar i anrop av potentiellt blockande funktioner, det går inte att schemalägga en coroutine på en annan OS-tråd, eventuella exceptions som når till "toppen" av en coroutine bara försvinner. Go har ingen av de begränsningarna vilket gör det klart enklare att jobba med då det fungerar "som man är van med".

Här är modifikationen som gör att content blir samma

Klicka för mer information

#include <iostream> #include <thread> #include <boost/asio/as_tuple.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/use_awaitable.hpp> #include <boost/beast/core.hpp> #include <boost/beast/http.hpp> namespace asio = boost::asio; namespace beast = boost::beast; namespace http = beast::http; namespace ip = asio::ip; using tcp = ip::tcp; std::string get_http_date() { auto now = std::chrono::system_clock::now(); auto time = std::chrono::system_clock::to_time_t(now); std::ostringstream oss; oss << std::put_time(std::gmtime(&time), "%a, %d %b %Y %H:%M:%S GMT"); return oss.str(); } asio::awaitable<void> handle_request(beast::tcp_stream &stream, http::request<http::string_body> request) { http::response<http::string_body> response(http::status::ok, request.version()); // Add the Date header (RFC 7231 format) response.set(http::field::date, get_http_date()); response.set(http::field::content_type, "text/html; charset=utf-8"); response.body() = "<h1>Hello, World!</h1>"; response.prepare_payload(); co_await http::async_write(stream, response, asio::use_awaitable); } asio::awaitable<void> poll_socket(tcp::socket socket) { http::request<http::string_body> request; beast::tcp_stream stream{std::move(socket)}; beast::flat_buffer buffer; for (;;) { auto [ec, size] = co_await http::async_read( stream, buffer, request, asio::as_tuple(asio::use_awaitable)); if (ec) { if (ec == http::error::end_of_stream) { break; } std::cerr << ec.message() << std::endl; break; } bool close{request.need_eof()}; co_await handle_request(stream, std::move(request)); if (close) { break; } request = {}; } stream.socket().shutdown(tcp::socket::shutdown_send); } asio::awaitable<void> poll_connections(asio::ip::address address, uint16_t port) { auto executor = co_await asio::this_coro::executor; tcp::endpoint endpoint{address, port}; tcp::acceptor acceptor{executor}; acceptor.open(endpoint.protocol()); acceptor.bind(endpoint); acceptor.listen(); tcp::socket socket{executor}; for (;;) { co_await acceptor.async_accept(socket, asio::use_awaitable); asio::co_spawn(executor, poll_socket(std::move(socket)), asio::detached); socket = tcp::socket{executor}; } } int main(int argc, char *argv[]) { const int threads = std::thread::hardware_concurrency(); asio::io_context ioc{threads}; asio::co_spawn(ioc.get_executor(), poll_connections(ip::make_address("0.0.0.0"), 8082), asio::detached); std::vector<std::thread> v(threads - 1); for (int i = 1; i < threads; i++) { v.emplace_back([&ioc] { ioc.run(); }); } ioc.run(); }

Visa mer

Och än en gång, uppskattar verkligen exemplet! Kul att se coroutines i action (hade aldrig använt dem innan, men hört en hel del om jobbet bakom från bl.a. CppCast postcast:en).

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

Danke! Var exakt något likt det där i alla fall jag ville se från @klk.

Jag kan inte Go, har det inte på datorerna så hade inte kunnat hjälpa till med att jämföra, däremot hade jag nog kunnat se om boost lösningen var optimalt gjord

MEN, hade jag skrivit samma tror jag det hade negligerats.

Permalänk
Datavetare
Skrivet av klk:

Jag kan inte Go, har det inte på datorerna så hade inte kunnat hjälpa till med att jämföra, däremot hade jag nog kunnat se om boost lösningen var optimalt gjord

MEN, hade jag skrivit samma tror jag det hade hade negligerats.

Frågan var ju efter en bättre presterande C++ version, inte Go version. Misstänkte att den C++/boost jag fick ihop inte på något sätt var optimal, men den gjorde i alla fall rätt sak.

Om du vill testa att göra en "stateful HTTP-server" vore det faktiskt kul att se hur man gör det inom ramen för stackless coroutines. Misstänker att det blir klart med komplicerat jämfört med en stateful Go version (en sådan är rätt simpel).

Man kan simulera ett gäng användare och någon "request data" t.ex. så här med WRK (peka ut skript med -s users.lua)

-- users.lua math.randomseed(os.time()) -- Seed random generator request = function() local user_id = math.random(1, 10000) local arg_val = math.random(1, 20) local url = "http://localhost/api?user=" .. user_id .. "&arg=" .. arg_val return wrk.format("GET", url) end

Man får då GET-anrop på formen

http://localhost/api?user=17&arg=42
http://localhost/api?user=66&arg=2
etc.
En superenkel stateful server skulle bara kunna summera arg-värdet per användare och skicka tillbaka det som svar: "user=17\nsum=3466".

Lösning med global tabell + lås fungerar, men lär inte skala speciellt bra. Kanske finns något riktigt spännande trick ihop med C++20 coroutines?

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

Om du vill testa att göra en "stateful HTTP-server" vore det faktiskt kul att se hur man gör det inom ramen för stackless coroutines. Misstänker att det blir klart med komplicerat jämfört med en stateful Go version (en sådan är rätt simpel).

Om dygnet hade 48 timmar så kanske och skulle jag göra den så hade jag inte gjort den från 0, har rätt mycket generell kod precis som stl för att pussla ihop saker.

Jag gillar inte boost och den främsta orsaken är att det är rejäla nördar som kodat det mesta där. Vad jag vet så är det endast ett fåtal som underhåller och kan underhålla koden. De är självklart fantastiskt duktiga men förstår inte att det mesta är i deras huvud.

Ett annat problem denna typ av kod som nära nog har samma krav som STL får. Koden måste fungera överallt. Det innebär att boost måste hantera sådant som kanske bara en av tiotusen någonsin kommer ha behov av.

För att ta ett exempel.
I stl finns en väldigt användbar klass som heter std::string_view. Den är så bra för att den underlättar utan att att kosta speciellt mycket i prestanda. std::string_view har endast en pekare till en buffer (char*) och dess längd. Så vad är problemet med det?
Längden på strängen är på en 64 bitars dator 8 byte, pekaren är också 8 byte. sizeof( std::string_view ) = 16

Kanon - 16 byte är en multipel på 4, det går in 4 stycken std::string_view på en cache line ( intel processorer ).

Sannolikheten att det behövs 8 byte för att hålla längden i en sträng finns inte idag, så mycket minne har inte datorer, 5 byte hade troligen klarat allt. Men klarar i princip alla scenarior med 4 byte, 4 byte skulle alltså i de allra flesta fall vara tillräckligt för sköta längden i std::string_view

När det gäller just std::string:view är detta inte ett problem, block om 16 byte är ofta att föredra framför 12 byte, det går fortfarande bara in 4 std::string_view på en cache line om den varit 12 byte.

I fallet std::string_view är det inget problem att de slarvar lite med minnesutrymmet.

Problemet uppstår när man gör klasser som behöver mer information än vad std::string_view behöver, en variabel till eller kanske mer ändå.

boost har exempelvis Boost.JSON. Om de inte ändrat senaste åren så varje element i deras JSON objekt är 48 byte. Jämför man med andra generella bibliotek skrivna för att hantera JSON så är deras json objekt nästan alltid 32 byte.

Hur kommer det sig att just boost slarvade så vilket också gör att man inte kan få in 2 json objekt på en cache line.

Förklaringen är just att boost måste anpassa sig för att klara ALLA situationer. Boost kan inte trixa lika mycket som andra bibliotek kan, samma med STL. STL är absolut supersnabbt men det finns gränser i vad de kan göra eftersom de måste klara av att hantera ALLA möjliga situationer.

Så varför då använda boost?
Tror inte det är så många idag som behöver det, de som plockar in boost gör det kanske för någon enstaka. Det finns självklart de som använder en hel del i boost men behovet är inte alls samma som för +5 år sedan. STL har också ätit upp en del av boost.
Största nackdelen med boost som de inte kan göra så mycket åt idag är att det är stort, att dela upp det i delar är ofta komplicerat. Dessutom så är koden till och med svårare att debugga än stl.

Kod för att skriva en webserver eller nätverk generellt är en styrka, Parser (tidigare spirit) tror jag också är mycket användbart för de som behöver den typen av logik. Har man mycket avancerade regex hantering kan det också vara värt. Annars finns det oftast enklare lösningar att välja på annat håll eller skriva koden själv.

Hade jag valt lösning för json trots att man eventuellt redan använder boost hade jag inte valt Boost.JSON, hade plockat in något annat.

Permalänk
Datavetare
Skrivet av klk:

Om dygnet hade 48 timmar så kanske och skulle jag göra den så hade jag inte gjort den från 0, har rätt mycket generell kod precis som stl för att pussla ihop saker.

Jag gillar inte boost och den främsta orsaken är att det är rejäla nördar som kodat det mesta där. Vad jag vet så är det endast ett fåtal som underhåller och kan underhålla koden. De är självklart fantastiskt duktiga men förstår inte att det mesta är i deras huvud.

Ett annat problem denna typ av kod som nära nog har samma krav som STL får. Koden måste fungera överallt. Det innebär att boost måste hantera sådant som kanske bara en av tiotusen någonsin kommer ha behov av.

För att ta ett exempel.
I stl finns en väldigt användbar klass som heter std::string_view. Den är så bra för att den underlättar utan att att kosta speciellt mycket i prestanda. std::string_view har endast en pekare till en buffer (char*) och dess längd. Så vad är problemet med det?
Längden på strängen är på en 64 bitars dator 8 byte, pekaren är också 8 byte. sizeof( std::string_view ) = 16

Kanon - 16 byte är en multipel på 4, det går in 4 stycken std::string_view på en cache line ( intel processorer ).

Sannolikheten att det behövs 8 byte för att hålla längden i en sträng finns inte idag, så mycket minne har inte datorer, 5 byte hade troligen klarat allt. Men klarar i princip alla scenarior med 4 byte, 4 byte skulle alltså i de allra flesta fall vara tillräckligt för sköta längden i std::string_view

När det gäller just std::string:view är detta inte ett problem, block om 16 byte är ofta att föredra framför 12 byte, det går fortfarande bara in 4 std::string_view på en cache line om den varit 12 byte.

I fallet std::string_view är det inget problem att de slarvar lite med minnesutrymmet.

Problemet uppstår när man gör klasser som behöver mer information än vad std::string_view behöver, en variabel till eller kanske mer ändå.

boost har exempelvis Boost.JSON. Om de inte ändrat senaste åren så varje element i deras JSON objekt är 48 byte. Jämför man med andra generella bibliotek skrivna för att hantera JSON så är deras json objekt nästan alltid 32 byte.

Hur kommer det sig att just boost slarvade så vilket också gör att man inte kan få in 2 json objekt på en cache line.

Förklaringen är just att boost måste anpassa sig för att klara ALLA situationer. Boost kan inte trixa lika mycket som andra bibliotek kan, samma med STL. STL är absolut supersnabbt men det finns gränser i vad de kan göra eftersom de måste klara av att hantera ALLA möjliga situationer.

Så varför då använda boost?
Tror inte det är så många idag som behöver det, de som plockar in boost gör det kanske för någon enstaka. Det finns självklart de som använder en hel del i boost men behovet är inte alls samma som för +5 år sedan. STL har också ätit upp en del av boost.
Största nackdelen med boost som de inte kan göra så mycket åt idag är att det är stort, att dela upp det i delar är ofta komplicerat. Dessutom så är koden till och med svårare att debugga än stl.

Kod för att skriva en webserver eller nätverk generellt är en styrka, Parser (tidigare spirit) tror jag också är mycket användbart för de som behöver den typen av logik. Har man mycket avancerade regex hantering kan det också vara värt. Annars finns det oftast enklare lösningar att välja på annat håll eller skriva koden själv.

Hade jag valt lösning för json trots att man eventuellt redan använder boost hade jag inte valt Boost.JSON, hade plockat in något annat.

Historiskt var boost rätt mycket en inkubator för "bra att ha saker" där de mest generellt användbara togs in i standardbiblioteket. Vet inte om det stämmer längre.

Men just coroutines är inte en boost-finess, boost verkar ha "stackful coroutines" och C++ ISO kommittén tittade både på "stackful" och "stackless" coroutines, valet föll på "stackless".

"Stackless" varianten har fler begränsningar, men man verkar ha ansett att de fungerar ändå bra ihop med de flesta I/O-intensiva uppgifter och man såg störst värde där.

"Stackful" coroutines är mer generella, då går i praktiken att använda "till allt". Men ett stort problem för C++ (och de flesta språk) är att det inte finns något enkelt sätt att växa stacken efter man startat en tråd/task -> man behöver ta höjd för "worst-case" vilket skulle gjort coroutines väl dyrt. Go "löser" detta genom att alltid starta med väldigt liten call-stack (typ 4k) som kan växa vid behov (något som kräver runtime-stöd och att man tänkt på det från start).

Angående std::stringview etc. Är helt med på hur cache och dess konsistensprotokoll fungerar, men hur relevant är det att mikro-optimera något sådan på moderna CPUer som dels kan ha >500 instruktioner "in-flight", ha >50 samtida minnesoperationer "in-fligh" samt kan göra rätt många load/store per cykel?

OK om man gör korkade saker, men lär knappast spela någon relevant roll om man använder 12 eller 16 bytes...

Vidare är det egentligen inget problem att packa in fler 12 bytes objekt i en cache-line. Sedan Sandy Bridge (2011) kostar det inget extra att läsa/skriva ens på udda alignment om man nu absolut måste optimera på den nivå (lär inte finnas någon modern high-end CPU som inte har dessa optimeringar idag).

Angående stateful server: i just detta läge skulle det nog ändå fungera helt OK med en vanlig std::unordered_map<userId, userData> och en mutex över den tabellen (ger inte speciellt bra skalning över kärnor, om nu inte servern gör något som är beräkningsmässigt dyrt för då lär det inte spela någon roll).

Det skrivet just sådant är väldigt enkelt i Go då http-servern enkelt kan ges "state", t.ex. så här

package main import ( "fmt" "net/http" "strconv" "sync" ) type StatefulServer struct { users sync.Map // safe to use from multiple go-routines without additional locking } type User struct { mtx sync.Mutex // per-user lock, sum could be an atomic instead in this trivial case sum int32 // other user data } func (serverState *StatefulServer) handler(w http.ResponseWriter, r *http.Request) { userStr := r.URL.Query().Get("user") argStr := r.URL.Query().Get("arg") userId, userIdOk := strconv.Atoi(userStr) arg, argOk := strconv.ParseInt(argStr, 10, 32) if userIdOk != nil || argOk != nil { http.Error(w, "Missing or invalid formatted parameters", http.StatusBadRequest) return } userEntry, _ := serverState.users.LoadOrStore(userId, &User{sum: 0}) user := userEntry.(*User) user.mtx.Lock() defer user.mtx.Unlock() user.sum += int32(arg) fmt.Fprintf(w, "Sum is %d for user %d", user.sum, userId) } func main() { port := 8080 serverState := StatefulServer{sync.Map{}} fmt.Printf("Server running on port %d...\n", port) http.HandleFunc("/", serverState.handler) err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) if err != nil { fmt.Println("Server error:", err) } }

Dold text

Edit: även med denna "stateful" HTTP fixar Orange Pi 5:an 88k transaktioner per sekund (mot 95k innan).

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

Det skrivet just sådant är väldigt enkelt i Go då http-servern enkelt kan ges "state", t.ex. så här

Ja, att synkronisera en lista är inte svårt i C++ heller men det fyller ingen större funktion. Det är ju först när man kopplar data och denna data kan konfigureras och skyddas (fungerar i en trådad) miljö som det blir användbart.
Det var detta jag menade med att bygga en webserver.

Bara dra ihop en server som har "något" state för användaren är gissningsvis enkelt i de flesta kompilerade språken som finns

Permalänk
Medlem

Här beskrivs det väldigt bra varför C och C++ utvecklare i någon mån inte har så mycket alternativ

Permalänk
Medlem
Skrivet av klk:

Här beskrivs det väldigt bra varför C och C++ utvecklare i någon mån inte har så mycket alternativ

Jag kan inte identifiera något som säger att C- och C++-utvecklare inte har några alternativ. Casey kodar själv i C++ så han har ju RAII och precis som han kan gå ifrån(minnespoola) RAII i C++ så kan du frångå RAII i Rust eller hantera saker mer C-likt i Zig.

Så varför har inte C- och C++-utvecklare alternativ? (Jag ser fortfarande inte enhörningen utan)

Utöver det så tycker jag att hans argument är aningen dumt. Precis som att det skulle vara nybörjarbeteende tänka på individuella allokeringar och först när man blivit en programmerargud och fått ett skrivbord bredvid Zeus på Olympus då hanterar man enbart minne i subpooler. Låter inte som han behöver optimera för minne och har scenario som lätt går att resonera i termer av subpooler.

När det kommer till design och arkitektur så är det en mer filosofisk diskussion och om någon säger att X är det ända rätta så bör man nog ta det med en nypa salt, subpooling precis som RAII, GC kommer med sina för- och nackdelar.

Permalänk
Medlem
Skrivet av orp:

Jag kan inte identifiera något som säger att C- och C++-utvecklare inte har några alternativ. Casey kodar själv i C++ så han har ju RAII och precis som han kan gå ifrån(minnespoola) RAII i C++ så kan du frångå RAII i Rust eller hantera saker mer C-likt i Zig.

Så varför har inte C- och C++-utvecklare alternativ? (Jag ser fortfarande inte enhörningen utan)

Utöver det så tycker jag att hans argument är aningen dumt. Precis som att det skulle vara nybörjarbeteende tänka på individuella allokeringar och först när man blivit en programmerargud och fått ett skrivbord bredvid Zeus på Olympus då hanterar man enbart minne i subpooler. Låter inte som han behöver optimera för minne och har scenario som lätt går att resonera i termer av subpooler.

När det kommer till design och arkitektur så är det en mer filosofisk diskussion och om någon säger att X är det ända rätta så bör man nog ta det med en nypa salt.

Det vanligaste open source biblioteket för att jobba med json i C++ tror jag är nlohmann ( https://github.com/nlohmann/json ). Orsaken är att det är så smidigt att jobba med om man också använder stl. Men det är långsamt, mycket långsammare än de flesta andra.
De andra som är snabbare har en hel del trick för sig och det är ofta trix för att jobba mer effektivt med minne.

Det normala är kanske att endast några få delar i program behöver det här, de är viktiga och återanvänds. Jag kan ta ett eget exempel, i koden jag jobbar med används två stycken olika objekt. Dessa används överallt och är rejält optimerade. Har mycket funktionalitet. Handlar om drygt 5 000 rader kod. Förutom de här två objekten kan resten i princip vara skriven i python. Det har så liten påverkan på hur snabb koden är.

Permalänk
Medlem
Skrivet av klk:

Det vanligaste open source biblioteket för att jobba med json i C++ tror jag är nlohmann ( https://github.com/nlohmann/json ). Orsaken är att det är så smidigt att jobba med om man också använder stl. Men det är långsamt, mycket långsammare än de flesta andra.
De andra som är snabbare har en hel del trick för sig och det är ofta trix för att jobba mer effektivt med minne.

Det normala är kanske att endast några få delar i program behöver det här, de är viktiga och återanvänds. Jag kan ta ett eget exempel, i koden jag jobbar med används två stycken olika objekt. Dessa används överallt och är rejält optimerade. Har mycket funktionalitet. Handlar om drygt 5 000 rader kod. Förutom de här två objekten kan resten i princip vara skriven i python. Det har så liten påverkan på hur snabb koden är.

Hur är detta relaterat till att C och C++ inte har några alternativ pga att någon spelutvecklare tycker om att använda för-allokerade minnesregioner för minneshantering?

Vad är det för alternativ C och C++ saknar?

Permalänk
Medlem
Skrivet av orp:

Vad är det för alternativ C och C++ saknar?

Smidigheten att jobba med minne och det är långt ifrån bara språket som man väljer utifrån det. Hela min utvecklingsmiljö är vald i syfte att kunna jobba med lösningar där man måste ha verktyg.
Så allt från språket, utvecklingsmiljön, operativsystem mm är valda baserat på att kunna producera optimerade lösningar och underhåll.

Vet du något annat språk med utvecklingsmiljö som är smidig att använda vid tråd och minneshantering?

Permalänk
Medlem
Skrivet av klk:

Smidigheten att jobba med minne och det är långt ifrån bara språket som man väljer utifrån det. Hela min utvecklingsmiljö är vald i syfte att kunna jobba med lösningar där man måste ha verktyg.
Så allt från språket, utvecklingsmiljön, operativsystem mm är valda baserat på att kunna producera optimerade lösningar och underhåll.

Vet du något annat språk med utvecklingsmiljö som är smidig att använda vid tråd och minneshantering?

Rust och Zig fungerar fint för min del sedan har jag inte kikat närmre på varken Hare, Odin, Jai, C3 eller Carbon.

Permalänk
Datavetare
Skrivet av klk:

Ja, att synkronisera en lista är inte svårt i C++ heller men det fyller ingen större funktion. Det är ju först när man kopplar data och denna data kan konfigureras och skyddas (fungerar i en trådad) miljö som det blir användbart.
Det var detta jag menade med att bygga en webserver.

Bara dra ihop en server som har "något" state för användaren är gissningsvis enkelt i de flesta kompilerade språken som finns

Missade nog detta svar...

Fast nu är det enda relevanta problemet löst: med CSP (vilket Go har "native" och som är trivialt att implementera med C++20 co-routines) så har man en multitrådad load-balancer som lurar ut vilken användare det är.

Efter den har man en go-routine / co-routine som hanterar exakt en användare med isolerad data -> inga multi-thread problem, samtidigt som man effektivt kan använda många kärnor så länge som antalet aktiva användare är "tillräckligt" stort.

I Go är användning av flera kärnor automatiskt då det sköts av runtime. I C++20 får man bygga en "executor" som schemalägger co-routiner på önskat sätt.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av orp:

Rust och Zig fungerar fint för min del sedan har jag inte kikat närmre på varken Hare, Odin, Jai, C3 eller Carbon.

Men du jobbar inte direkt med minnet. Gör du inte det så behöver du inte tänka på en del.

Web:
Skriver jag web så kör jag sublime text och använder grok för att generera saker. Nästan alla använder VS Code. Men den ger för mycket hjälp i fel saker om man skriver imperativ kod för webben.

C++:
VS Code och debugga stora C++ projekt eller bara navigera i stora C++ projekt är nära dubbelt så segt som visual studio.

Permalänk
Datavetare
Skrivet av klk:

Här beskrivs det väldigt bra varför C och C++ utvecklare i någon mån inte har så mycket alternativ
https://www.youtube.com/watch?v=xt1KNDmOYqA

Herregösses: "jag är en spelutvecklare som upptäckt arena-allokatorer, nu är min åsikt att alla som inte fattar hur bra arena-allokator är n00bs".

Och ja, vet vem personen är. En rätt vanlig kritik jag sett mot honom är just att han må vara väldigt duktig inom sitt specifika område, men han verkar ha väldigt dålig koll på vad som är de relevanta flaskhalsarna i andra krävande områden. Men ändå lyfter han fram sina egna erfarenheter som att de är helt allmängiltiga (skulle vara spännande att se hans "superoptimeringar" applicerade i en starkt multitrådad miljö där livslängden inte är 100 % deterministisk...)

Han har också rätt ordentligt missförstått RAII. Det är just RAII, inte MAII. Minne är bara en resurs av många som väldigt smidigt och effektivt kan hanteras via RAII. För att ta Go som exempel igen, där finns inga destruktorer (finns överhuvudtaget inget explicit OOP stöd, vilket i.m.n.h.o är en av fördelarna med språket) men man har ett specifikt nyckelord just för att kunna implementera RAII (ett som TrapC verkar "stjäla" för att få in RAII stöd även där).

Arena-allokator är otroligt bra i renderings-steget i spel då så mycket upphör att leva efter man skickat allt till GPU varje frame. Arena-allokator är långt ifrån allmängiltigt bra, det är ett verktyg av många och även om det finns flera bra use-case utanför just 3D-rendering i spel är det totalt sett en nischfunktion.

Det är inte heller något som är bundet till C och C++, det är precis lika relevant att använda tekniken i spel som använder C#.

Vidare är inte smarta pekare och arena-allokatorer motsatser. Tvärtom finns en rad fördelar med att använda smarta pekare (d.v.s. utnyttja RAII, går inte att använda std::shared_ptr<> eller std::unque_ptr<> här). Ett sådant är att man då kan växla mellan arena-allokator och vanlig heap-allokator utan att ändra något mer än implementationen av den smarta pekaren. Även när man kör arena-allokator varianten finns fördelar med RAII, t.ex. att i debug-buggen verifiera att destruktor körs innan hela arenan går out-of-scope (gör den inte det har man en rätt svårhittad uses-after-free bug som i stort sätt inga verktyg som bygger på statisk analys kommer hitta då minnet sett från OS/runtime faktiskt är allokerat hela tiden).

Slutligen: just det som gör arena-allokatorer så snabba, extremt simpel logik för att "allokera" X bytes minne, är exakt vad som gör att GC-språk i vissa lägen kan vara snabbare än C, C++ och Rust om/när man har logik som allokerar väldigt många små objekt. Heap-allokering med GC är rätt mycket en arena-allokator i sin allok-logik. Skillnaden är att de flesta GC-språk kan kompaktera sin heap vid behov vilket undviker extern fragmentering, något som kan bli ett rätt svårt problem i C, C++ och Rust (är specifikt inte ett problem när man kör arean-allokator då den alltid ger upp hela minnet varje gång).

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av klk:

Men du jobbar inte direkt med minnet. Gör du inte det så behöver du inte tänka på en del.

Jag vet inte hur många gånger vi har varit över detta ... börjar bli aningen tjatigt ...
Du kan pilla på minnet i Rust, tror du själv att dom hade tillåtit drivare skrivna i Rust till Linux om du inte kunde kontrollera minnet?

Här är motsvarande memset:
https://doc.rust-lang.org/std/ptr/fn.write_bytes.html

Här är motsvarande memcpy:
https://doc.rust-lang.org/std/ptr/fn.copy_nonoverlapping.html

Kan du vänligen komma ihåg detta nu?

Skrivet av klk:

Web:
Skriver jag web så kör jag sublime text och använder grok för att generera saker. Nästan alla använder VS Code. Men den ger för mycket hjälp i fel saker om man skriver imperativ kod för webben.

C++:
VS Code och debugga stora C++ projekt eller bara navigera i stora C++ projekt är nära dubbelt så segt som visual studio.

Skoj för dig. Jag använder Neovim till det mesta.

Permalänk
Medlem
Skrivet av Yoshman:

Slutligen: just det som gör arena-allokatorer så snabba, extremt simpel logik för att "allokera" X bytes minne, är exakt vad som gör att GC-språk i vissa lägen kan vara snabbare än C, C++ och Rust om/när man har logik som allokerar väldigt många små objekt. Heap-allokering med GC är rätt mycket en arena-allokator i sin allok-logik. Skillnaden är att de flesta GC-språk kan kompaktera sin heap vid behov vilket undviker intern fragmentering, något som kan bli ett rätt svårt problem i C, C++ och Rust (är specifikt inte ett problem när man kör arean-allokator då den alltid ger upp hela minnet varje gång).

Där slutar i princip alla som är vana att skriva optimerad C++ kod att lyssna på dig.
GC är seegt, jag begriper inte att du som annars verkar vara mycket påläst inte lärt dig det

Permalänk
Medlem

Nu när vi än en gång fastställt att du kan pilla på minne i Rust och Zig och troligen en hög med andra språk utöver C och C++ så undrar jag vilka alternativ som C- och C++-utvecklare saknar eller det var det hela?

Permalänk
Medlem
Skrivet av orp:

Skoj för dig. Jag använder Neovim till det mesta.

Jag har svårt för alla tangentbordkombinationer eftersom man nära nog måste hålla sig till en enda editor för att ha koll och den är jobbig att debugga med.
Men absolut, de som tränar upp muskelminne kan bli snabba på att skriva kod. Men att skriva kod snabbt är inte samma sak som att vara smabb utvecklare.

Permalänk
Datavetare
Skrivet av klk:

Där slutar i princip alla som är vana att skriva optimerad C++ kod att lyssna på dig.
GC är seegt, jag begriper inte att du som annars verkar vara mycket påläst inte lärt dig det

Bara om man tror att en GC 2025 fungerar på exakt samma sätt som en GC gjorde 1995.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av orp:

Nu när vi än en gång fastställt att du kan pilla på minne i Rust och Zig och troligen en hög med andra språk utöver C och C++ så undrar jag vilka alternativ som C- och C++-utvecklare saknar eller det var det hela?

Kan är inte samma sak som att det är smidigt.
Du kan koda C++ i VS Code med. Men kör WSL med VS Code utan att få hjärnblödning för att det tar några sekunder innan editorn reagerar

Permalänk
Medlem
Skrivet av Yoshman:

Bara om man tror att en GC 2025 fungerar på exakt samma sätt som en GC gjorde 1995.

Det handlar inte om hur GCn fungerar. Du gör helt andra lösningar utan GC

Permalänk
Medlem
Skrivet av klk:

Kan är inte samma sak som att det är smidigt.

Jag upplever inget osmidigt med det.

Skrivet av klk:

Du kan koda C++ i VS Code med. Men kör WSL med VS Code utan att få hjärnblödning för att det tar några sekunder innan editorn reagerar

Jag har ingen erfarenhet av varken WSL eller VS Code så hänger inte med på detta resonemanget.

Permalänk
Medlem
Skrivet av klk:

Det handlar inte om hur GCn fungerar. Du gör helt andra lösningar utan GC

Du känner till att alla moderna cloudlösningar och majoriteten av datacenter använder sig av clusterlösningar som är byggda med GC-språk(Golang)?

Permalänk
Medlem
Skrivet av klk:

Där slutar i princip alla som är vana att skriva optimerad C++ kod att lyssna på dig.
GC är seegt, jag begriper inte att du som annars verkar vara mycket påläst inte lärt dig det

Vissa av sakerna du skriver är så arroganta och ofattbart korkade.

@Yoshman har lagt så jäkla mycket energi på att förklara resonemanget för dig och är uppenbart mer påläst och allt du gör är att stoppar fingrarna i öronen och säger något i stil med "na na na na jag kan inte höra dig.... GC är dåligt för C++ är min enhörning ...".

Permalänk
Medlem
Skrivet av orp:

Jag har ingen erfarenhet av varken WSL eller VS Code så hänger inte med på detta resonemanget.

Det var inte poängen utan jag försökte förklara hur olika utvecklare väljer det som passar dem bäst. Det skiljer mellan olika programmerare vad de behöver. Om jag endast skall skriva html hade jag förmodligen också använt vs code för där är den stark. Samma sak med olika ramverk som react och liknande. VS Code är nästan alltid den som först får tillägg för olika system.

Skriver någon Rust kod har den inte samma behov som den som skriver C++ kod, samma med C#, Java eller något annat.
Skiljer dessutom trots att man använder samma språk för utvecklare kan skriva olika typer av lösningar.

Problemet som jag tror jag och flera andra antytt tidigare i tråden handlar om vad många C/C++ inte förstår. Och de förstår inte eftersom området för dem är så lätt. Andra utvecklare som jobbar med annat och har andra prioriteringar borde lyssna bättre och inte tro att alla tänker som dem.

Då vi diskuterat rust vs C och en del linux. Jag tycker det är intressant hur den soppan kommer sluta. Rust "hatar" att jobba med minnet, något som de flesta C programmerare tycker är en styrka med språket. Rust utvecklare kritiserar C utvecklare för att C språket är dåligt just på grund av det. Och dessa två grupper skall jobba TILLSAMMANS i linux, det kommer aldrig gå.

Permalänk
Medlem
Skrivet av orp:

Vissa av sakerna du skriver är så arroganta och ofattbart korkade.

@Yoshman har lagt så jäkla mycket energi på att förklara resonemanget för dig och är uppenbart mer påläst och allt du gör är att stoppar fingrarna i öronen och säger något i stil med "na na na na jag kan inte höra dig.... GC är dåligt för C++ är min enhörning ...".

Såg du videon?

Men absoult, du kan göra ett jätteblock med en GC och använda offset positioner. Den koden är inte rolig att jobba med.

Permalänk
Medlem
Skrivet av klk:

Det handlar inte om hur GCn fungerar. Du gör helt andra lösningar utan GC

Jaså? På vilket sätt, och varför skulle inte de lösningarna fungera med GC?

Permalänk
Datavetare
Skrivet av klk:

Det handlar inte om hur GCn fungerar. Du gör helt andra lösningar utan GC

Du menar väl "utan GC måste man lösa det på andra sätt för att inte få bedrövlig prestanda i ett språk vars huvudpoäng är bra prestanda"? Och i nästan alla custom-memory-allocator trix går att göra även om man har GC.

I detta fall skulle man självklart göra en custom-memory-allocator lösning i C++ om det var ett "riktigt" problem. Men bara för att få en känsla för just hur mycket bättre en modern GC är med en naiv approach jämfört med en logiskt identisk C++ lösning kan vi kika på detta

Detta kommer från en microbenchmark för minneshantering, så är lite poängen att det ska vara mycket små allokeringar.

#include <iostream> #include <memory> #include <cmath> struct Node { Node *left; Node *right; Node(Node *l = nullptr, Node *r = nullptr) : left(l), right(r) {} ~Node() { delete left; delete right; } int itemCheck() const { if (!left) return 1; return 1 + left->itemCheck() + right->itemCheck(); } }; Node *bottomUpTree(int depth) { if (depth <= 0) return new Node(); return new Node(bottomUpTree(depth - 1), bottomUpTree(depth - 1)); } const int minDepth = 4; int main(int argc, char *argv[]) { int n = 0; if (argc > 1) { n = std::stoi(argv[1]); } int maxDepth = n; if (minDepth + 2 > n) { maxDepth = minDepth + 2; } int stretchDepth = maxDepth + 1; Node *stretchTree = bottomUpTree(stretchDepth); int check = stretchTree->itemCheck(); std::cout << "stretch tree of depth " << stretchDepth << "\t check: " << check << "\n"; delete stretchTree; Node *longLivedTree = bottomUpTree(maxDepth); for (int depth = minDepth; depth <= maxDepth; depth += 2) { int iterations = 1 << (maxDepth - depth + minDepth); check = 0; for (int i = 1; i <= iterations; i++) { Node *tempTree = bottomUpTree(depth); check += tempTree->itemCheck(); delete tempTree; } std::cout << iterations << "\t trees of depth " << depth << "\t check: " << check << "\n"; } std::cout << "long lived tree of depth " << maxDepth << "\t check: " << longLivedTree->itemCheck() << "\n"; delete longLivedTree; return 0; }

C++

package main import ( "flag" "fmt" "strconv" ) var n = 0 type Node struct { left, right *Node } func bottomUpTree(depth int) *Node { if depth <= 0 { return &Node{} } return &Node{bottomUpTree(depth - 1), bottomUpTree(depth - 1)} } func (n *Node) itemCheck() int { if n.left == nil { return 1 } return 1 + n.left.itemCheck() + n.right.itemCheck() } const minDepth = 4 func main() { flag.Parse() if flag.NArg() > 0 { n, _ = strconv.Atoi(flag.Arg(0)) } maxDepth := n if minDepth+2 > n { maxDepth = minDepth + 2 } stretchDepth := maxDepth + 1 stretchTree := bottomUpTree(stretchDepth) check := stretchTree.itemCheck() fmt.Printf("stretch tree of depth %d\t check: %d\n", stretchDepth, check) longLivedTree := bottomUpTree(maxDepth) for depth := minDepth; depth <= maxDepth; depth += 2 { iterations := 1 << uint(maxDepth-depth+minDepth) check = 0 for i := 1; i <= iterations; i++ { tempTree := bottomUpTree(depth) check += tempTree.itemCheck() } fmt.Printf("%d\t trees of depth %d\t check: %d\n", iterations, depth, check) } fmt.Printf("long lived tree of depth %d\t check: %d\n", maxDepth, longLivedTree.itemCheck()) }

Go

Go versionen är på min datorn (M3 Max) nästan 10 gånger snabbare!!! Bygger med clang 16.0 och -O2 för C++, och för Go är det 1.24.1.

Man lär se samma resultat om Go byts ut mot moderna varianter av C# eller Java.

C++ kan vara effektivt, framförallt när man bygger enkeltrådad programvara där man då bara har logik att tänka på i sina custom-memory allocators. Inte lika roligt att försöka bygga något icke-trivial, använda många CPU-kärnor och fortfarande ha det väldigt effektivt...

Overhead att använda GC minskar rejält så fort man går multicore. Vissa (multicore-aware)algoritmer går att göra både enklare och effektivare när man har GC.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer