Jump to content

  • Log In with Google      Sign In   
  • Create Account

Go language use in performance-critical code


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
16 replies to this topic

#1 Nairou   Members   -  Reputation: 431

Like
0Likes
Like

Posted 08 December 2012 - 11:30 AM

Coming from a C/C++ perspective, Go looks like a really nice language. I would really like to try it out. I'm curious though if anyone has considered it for game development?

Other than a lack of library support (which will improve over time), my main concern with the language is how it handles memory management. It has a GC, and appears to have a lot of dynamic data types. I can only assume that this means it will be doing memory allocation at runtime. At least, that's what a C/C++ program would be doing. It is possible that Go is structured differently internally to allow seeming-dynamic features within a fixed memory allocation. But I don't know, and so hesitate to try it in performance-critical code, like games.

Does anyone have any insight into how Go handles memory management? Has anyone experimented with Go in performance-critical code and have feedback on how it performed?

Sponsor:

#2 kunos   Crossbones+   -  Reputation: 2207

Like
3Likes
Like

Posted 08 December 2012 - 12:01 PM

Ya I am playing with it in my spare time... it's by far my favorite language.
I don't understand what you mean for "dynamic types".. Go has no dynamic types, it's a very simple language statically typed... actually it is very strict about types, requiring explicit casts everywhere. Maybe you are confused by interfaces and duck typing.. but those aren't strictly "dynamic" features... most of them are evaluated at compile time.
Memory management is not very different from C like languages, so, from a performance point of view, the same rules are valid.. avoid creating things on the heap if you can... that's about it... the best way to optimize on a GC is to avoid triggering it.

Performance wise, from my tests on Windows (game oriented stuff like graph traversals and basic math) is in the same ballpark with C# using the standard go compiler. On Linux you can use the gccgo compiler built over the gcc complier and that's pretty damn fast... and runtime performances are only going to get better due the nature of the language.

For gamedev, Go is severely limited in expressing elegant math code.. it doesn't have operator overloading nor function overloading.. thus you're back writing things like:

result := mymath.AddFloat3(&v1 , &v2)

Instead of:

auto result=v1 +v2;

Things get worse with complex formulas:

auto new_pos=pos+vel * dt + (acc * 0.5f * dt *dt);

You'd have to break that down in go operation by operation if pos, vel and acc are vectors.

This gets really annoying when you write math intensive code... but you get lots of good stuff from the language such as goroutines and channels, so it's the goods and the bads are balancing out.

Edited by kunos, 08 December 2012 - 12:09 PM.

Stefano Casillo
Lead Programmer
TWITTER: @KunosStefano
AssettoCorsa - netKar PRO - Kunos Simulazioni

#3 Nairou   Members   -  Reputation: 431

Like
0Likes
Like

Posted 08 December 2012 - 12:17 PM

I don't understand what you mean for "dynamic types".. Go has no dynamic types, it's a very simple language statically typed... actually it is very strict about types, requiring explicit casts everywhere. Maybe you are confused by interfaces and duck typing.. but those aren't strictly "dynamic" features... most of them are evaluated at compile time.

What I mean is, things like strings and maps, which are variable in size. They don't have a fixed memory footprint at compile time.

Memory management is not very different from C like languages, so, from a performance point of view, the same rules are valid.. avoid creating things on the heap if you can... that's about it... the best way to optimize on a GC is to avoid triggering it.

I guess part of my question is wondering how you avoid it. I haven't read much on how Go handles it's memory management, and it seems a lot less transparent than it was in C/C++.

In C++, if you avoid malloc() and new, and don't use objects which are known to do runtime allocation, then you're good. With Go, I don't know what that set is. As great as Go looks, I'm curious how much of the language you have to avoid using if you want to keep tight control over what memory allocation occurs. There is probably a document out there somewhere that talks about this, but I haven't seen it.

Performance wise, from my tests on Windows (game oriented stuff like graph traversals and basic math) is in the same ballpark with C# using the standard go compiler. On Linux you can use the gccgo compiler built over the gcc complier and that's pretty damn fast... and runtime performances are only going to get better due the nature of the language.

Do you mean that gccgo compiles really fast, or that the resulting code is much faster than the standard go compiler? Do you have (or know of) any benchmarks?

For gamedev, Go is severely limited in expressing elegant math code.. it doesn't have operator overloading nor function overloading..

Good to know! I hadn't considered that bit. But definitely worth considerign still, given all of the other language benefits.

#4 Telastyn   Crossbones+   -  Reputation: 3730

Like
0Likes
Like

Posted 08 December 2012 - 01:14 PM

What I mean is, things like strings and maps, which are variable in size. They don't have a fixed memory footprint at compile time.


Unlike C++, which uses no strings or maps or variable size structures in non-trivial programs...

#5 6677   Members   -  Reputation: 1058

Like
0Likes
Like

Posted 08 December 2012 - 04:02 PM

With today's release of Go, I thought I would write a version of GenPrime for it, one because I wanted to see if it was really as speedy as it says and two to practice this new language.
First, the C version as a comparison:
Code:
eddie@ganglion:~/dev/genprime$ ./genprime-c 250000 1000000
Found 250000 primes in 5.75923 seconds (last was 3497861)
Found 500000 primes in 16.15777 seconds (last was 7368787)
Found 750000 primes in 29.94442 seconds (last was 11381621)
Found 1000000 primes in 46.37428 seconds (last was 15485863)
Now for Go:
Code:
eddie@ganglion:~/dev/genprime$ ./genprime-go 250000 1000000
Found 250000 primes in 4.253223 seconds (last was 3497861)
Found 500000 primes in 12.105688 seconds (last was 7368787)
Found 750000 primes in 22.779920 seconds (last was 11381621)
Found 1000000 primes in 34.426428 seconds (last was 15485863)

I compiled the Go version with the Plan 9 compiler (6g). Apparently there is a GCC port that is reported to produced more optimized code in some cases, so if anyone wants to test this code with the GCC port I'd be interested to see the results.
I'll be committing my code to my GitHub fork of GenPrime.


So not lagging too far behind plain C in that particular test case.

I can't recall where but I vaguely remember finding a .pdf with a few more test cases with very similar results.

There is also this page I found but I don't really know whats going on here: http://golang.org/test/bench/shootout/

#6 iMalc   Crossbones+   -  Reputation: 2314

Like
0Likes
Like

Posted 08 December 2012 - 04:38 PM


What I mean is, things like strings and maps, which are variable in size. They don't have a fixed memory footprint at compile time.


Unlike C++, which uses no strings or maps or variable size structures in non-trivial programs...

In case anyone hadn't picked up on this, the above was clearly sarcasm.


Also, GC is not a memory leak panacea. Our newer client software at work which is written in C#, "leaks" far worse than the older C++ client.
"In order to understand recursion, you must first understand recursion."
My website dedicated to sorting algorithms

#7 Nairou   Members   -  Reputation: 431

Like
-1Likes
Like

Posted 08 December 2012 - 07:50 PM


What I mean is, things like strings and maps, which are variable in size. They don't have a fixed memory footprint at compile time.

Unlike C++, which uses no strings or maps or variable size structures in non-trivial programs...

Well in C++ those are just part of STL, not build into the language. You can choose to use a std::string, or you can just use a char array. You aren't forced to use a string, because it isn't actually part of the language. In Go, you just have a string, and it's a part of the language, so they expect you to use it. Which is fine, I just want to understand what's going on behind the scenes.

Also, GC is not a memory leak panacea. Our newer client software at work which is written in C#, "leaks" far worse than the older C++ client.

Exactly. I have more memory leaks in my C# applications than I do in my C++ applications. I want to have some level of control over memory management, especially in a game. I just don't know enough about Go yet to know if it gives that level of control.

#8 Telastyn   Crossbones+   -  Reputation: 3730

Like
1Likes
Like

Posted 08 December 2012 - 08:10 PM

Sorry, the STL hasn't been around for years. The C++ standard library is part of the language, and that includes strings.

To be explicit, you need not worry about those things in go for performance concerns. Frankly, it sounds as though you worry about using them due to performance in C++, which is absurd.

#9 kunos   Crossbones+   -  Reputation: 2207

Like
0Likes
Like

Posted 09 December 2012 - 02:55 AM

Do you mean that gccgo compiles really fast, or that the resulting code is much faster than the standard go compiler? Do you have (or know of) any benchmarks?


gccgo compile times are not as fast as the standard go compiler but still way faster than C++ code. I was refering to runtime performances where gccgo is surprisingly fast.

I don't understand your obsession with dynamic memory. Every language needs it. Strings, maps, vectors in C++ are all implemented with dynamic memory behind the scenes. Go doesn't move stuff into dynamic memory behind your back. So you know that callig "new" will be a dynamic allocation, calling make will be a dynamic allocation, returing the address of a stack created object will be a dinamic allocation.. and operations like "append" or adds to a map might be dynamic allocs... this is all EXACTLY what you get in every other language.
Stefano Casillo
Lead Programmer
TWITTER: @KunosStefano
AssettoCorsa - netKar PRO - Kunos Simulazioni

#10 Malevolence   Members   -  Reputation: 323

Like
1Likes
Like

Posted 09 December 2012 - 11:51 AM

I work with Go on a daily basis, but never in game development. I've never done any benchmarking but it's definitely performant enough for any web server (Google does use it for production web servers) and the amazingly speedy compile time alone is enough to make up for any minor performance downfalls IMO.

To comment on your specific question about string vs char[], you can have fixed-size byte arrays in Go and cast between them and strings. In general I find Go a great language, but the libraries for it are still very immature.

#11 6677   Members   -  Reputation: 1058

Like
0Likes
Like

Posted 11 December 2012 - 03:11 PM

I've not seen the 2 (or 3) compared but .net and the JVM both manage to do ok on modern hardware and they must be far slower than Go. I think the only issue with Go is the lack of libraries, a problem which can be resolved.

#12 Cornstalks   Crossbones+   -  Reputation: 6991

Like
0Likes
Like

Posted 11 December 2012 - 03:33 PM



What I mean is, things like strings and maps, which are variable in size. They don't have a fixed memory footprint at compile time.


Unlike C++, which uses no strings or maps or variable size structures in non-trivial programs...


Well in C++ those are just part of STL, not build into the language. You can choose to use a std::string, or you can just use a char array. You aren't forced to use a string, because it isn't actually part of the language. In Go, you just have a string, and it's a part of the language, so they expect you to use it. Which is fine, I just want to understand what's going on behind the scenes.


To expand on Telastyn's response: std::string, std::map, std::vector, etc. absolutely are part of the language (see section 21 for the Strings library, section 23 for the Containers library, etc.). They just aren't primitive data types.

As for "STL"... The "Standard Template Library" was written before C++ was ever actually standardized, and is different from the C++ Standard Library. When C++ was finally standardized in 1998, it incorporated parts (but not the whole thing!) of the popular STL into the standard itself (and the C++ standard defines what we call the "standard library"). The STL is not, however, fully incorporated into the standard library (for example, the STL's rope never made it into the standard library), and there are some differences between the STL and the standard library (aside from the fact that the standard library adds more than was originally in the STL). Instead of saying "the STL is the standard library," we should say "the STL influenced the creation of the standard library." Unless you're specifically referring to the Standard Template Library that was written by Alexander Stepanov and Meng Lee before C++ was ever standardized, you probably mean (and should say) the C++ Standard Library Posted Image

Edited by Cornstalks, 11 December 2012 - 03:41 PM.

[ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

#13 achild   Crossbones+   -  Reputation: 1941

Like
1Likes
Like

Posted 11 December 2012 - 03:38 PM

There is also this page I found but I don't really know whats GOing on here: http://golang.org/test/bench/shootout/

har har

#14 Nairou   Members   -  Reputation: 431

Like
-1Likes
Like

Posted 11 December 2012 - 04:02 PM

To expand on Telastyn's response: std::string, std::map, std::vector, etc. absolutely are part of the language (see section 21 for the Strings library, section 23 for the Containers library, etc.). They just aren't primitive data types.

As for "STL"... The "Standard Template Library" was written before C++ was ever actually standardized, and is different from the C++ Standard Library. When C++ was finally standardized in 1998, it incorporated parts (but not the whole thing!) of the popular STL into the standard itself (and the C++ standard defines what we call the "standard library"). The STL is not, however, fully incorporated into the standard library (for example, the STL's rope never made it into the standard library), and there are some differences between the STL and the standard library (aside from the fact that the standard library adds more than was originally in the STL). Instead of saying "the STL is the standard library," we should say "the STL influenced the creation of the standard library." Unless you're specifically referring to the Standard Template Library that was written by Alexander Stepanov and Meng Lee before C++ was ever standardized, you probably mean (and should say) the C++ Standard Library Posted Image

Very interesting! :)

The point I was trying to make is that, being part of the standard library and not a core part of the language itself, they are optional. You can use a std::string if you want, with it's pro's and con's, or you can use a char*. The latter is a direct part of the language, and is very explicit and simple, while the former is part of a library and can have side effects (i.e. dynamic memory allocation behind the scenes). Once you understand how std::string works, it isn't a big deal. It's memory allocation is just a part of it's pros/cons list. But it's something to be learned.

Likewise, as someone who doesn't know Go very well, I was curious about which parts of the Go language have these sorts of memory allocation side effects. While std::string is an optional library component in C++, it appears to be a core part of the language in Go (you don't need to import anything to use a string). This makes it harder, on the surface, to know where these side effects are.

(And to those who think I'm being obsessive, read this.)

#15 Telastyn   Crossbones+   -  Reputation: 3730

Like
4Likes
Like

Posted 11 December 2012 - 04:33 PM

That's the biggest load of garbage I've read in a long time.

It is FAR more likely that you're going to overlook some bound or overflow some buffer than you kneecap your game because you ran out of memory via the boogeyman of inefficient standard library structures.

#16 larspensjo   Members   -  Reputation: 1557

Like
4Likes
Like

Posted 12 December 2012 - 08:42 AM

I am using Go for a MMORPG game. The server side is coded in Go, but the client side is coded in C++. The reason for using C++ on the client side was the lack of support libraries when the project started 2 years ago. I may have done it differently if I started with the client today.

Some details of the server design:
  • Every connected player has a goroutine of its own.
  • Quadtree algorithms (one for players and one for creatures) are used to avoid quadratic costs of information distribution.
  • As far as possible, tasks are organized as separate goroutines to enable scaling with more CPU cores.
  • Communication between goroutines is done, where feasible, in the common way of using channels instead of semaphores.
  • There are data (player information, creature data, world geography, etc.) that need to be accessed by many readers. Those cases are implemented using rwmutex.
  • To minimize the need of using more than one semaphore at the same time (for resource A and B), some cases have been possible to serialize, for example, by having a first "read-only" pass that identifies objects in A, creating a closure that does the job, and as a next step iterating that function over B (sometimes sending the closure on a channel to the responsible goroutine).
  • Player persistent data is managed in a SQL database, now being replaced by MongoDB.
  • World geography data is aggregated in chunks of 32x32x32 blocks, saved in a compressed format on the file system.
  • Chunks that have not been modified can be uniquely reconstructed by the terrain generator, independent on neighbor chunks. A chunk is cached at 4 different levels (including client+server), to minimize workload and communication, with special mechanisms to ensure consistency.
Given that the system has no logical internal boundaries, and that the world itself is very dynamic, it is difficult to support sharding. It could be done using "instancing", but has not been further elaborated for now. Source code for the server is available at https://github.com/larspensjo/ephenation-server and the client at https://github.com/larspensjo/ephenation-client.

Regarding questions of performance, the server is aggressively targeted to support 10,000 simultaneous players. This has not been verified, so I can't prove that it works. I have done some synthetic tests with 1000 players, giving a load on a Core I7 at about 10%. There are some algorithms that need to be improved to scale from 1,000 to 10,000 players, though. As mentioned elsewhere, one way to avoid high cost GC is to allocate less dynamic memory. Go is a little tricky about that, as it is not always obvious what data will be on the heap and what data will be on the stack. Especially as the compiler has some abilities to optimize (detect if data need to remain when functions is going out of scope). But there are simple things that can be done, like re-use of buffers instead of throwing them away for the GC.

The game server is not a "hard real time system", but players will sure take notice if there are delays above a certain threshold. I have various timers running to measure delays, and it "holds up" so far. A RPG game can usually allow for higher latencies than shooters.
Current project: Ephenation.
Sharing OpenGL experiences: http://ephenationopengl.blogspot.com/

#17 ApochPiQ   Moderators   -  Reputation: 16397

Like
2Likes
Like

Posted 12 December 2012 - 11:11 AM

How heavy is your world simulation though? The difference between 1k and 10k players on a server isn't necessarily linear, especially if you have any nontrivial simulation being done per-tick. Keep in mind that a lot of other factors can grow nonlinearly as well, such as dispatching socket traffic to various handlers. Especially as thread contention for shared resources increases, things can degrade at very unpredictable points.

In short, I'd caution heavily against assuming that 10% CPU at 1k players implies 10k potential players. 10k is a very, very ambitious target unless your game simulation is borderline trivial. I'd strongly recommend doing genuine load tests: the fun part of network systems is that they tend to fall over in ways you did not anticipate. Usually that means sooner than you hoped ;-)




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS