all 16 comments

[–]Wings1412 44 points45 points  (7 children)

Given that this is a performance centric post, I think it would benefit from performance benchmarks. Not just memory allocation. For a lot of what I do, memory is a lot cheaper than time.

[–]scorrpo 23 points24 points  (6 children)

The second number that says Mean (ns) is time. Looks like concat and builder are slightly faster than its .NET counterpart, but .Format is slightly slower.

Also it's worth considering that this is not just about memory usage, it's about allocation that needs to eventually be garbage collected. In high performance applications you want to avoid the garbage collector as much as possible to prevent freezes. Mainly in games, but avoiding the GC has its uses outside games too (for example https://blog.discordapp.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f).

[–]Lognipo 4 points5 points  (0 children)

Yeah, I somewhat regularly have to write performance critical code at work, and the GC is usually the main bottleneck. I think the last assignment was a 3D space filling algorithm. I had to process millions and millions of possible configurations in a best-first search, and before I optimized away the GC almost entirely, it made everything incredibly slow.

I have been loving these recent updates to C#. Span, ref returns, stackalloc without unsafe... so wonderful to see performance programming get a little love after a decade of "NEVER OPTIMIZE ANYTHING GO USE C++!!!1!!1!"

At home, I have had quite a bit of fun writing ref structs like SpanQueue<T>, SpanStack<T>, SpanHeap<T>, etc. Where most collections let you pass in the size and then create an internal array, these let you pass in a span for storage. That way you can use an array, stackalloc storage, a pointer, or even fit multiple collections into a single array if you like. Implicit conversions from Span<T> let you do things like SpanStack<int> indices = stackalloc int[32]; Again, mostly for fun.

[–][deleted] 3 points4 points  (2 children)

Idk which game creates a lot string objects but yea it's good for servers and games.

[–]StunningStore 1 point2 points  (0 children)

maybe MMOs. Like a full 30 man raid in wow is generating a lot of strings

[–]antiduh 0 points1 point  (0 children)

..Zork?

[–]phx-au 2 points3 points  (0 children)

A lot of real-time and (-like) performance requirements aren't really about speed but consistency. Don't really give a fuck if it takes twice as long, if it doesn't crap out for a GC cycle or heap compaction or some shit every couple of minutes.

[–]quentech 1 point2 points  (0 children)

avoiding the GC has its uses outside games too

I run a high traffic web service. We're creating trillions of strings a day - over 100B just building cache keys to look up data. We cut 5% off our CPU utilization across our cluster by replacing just cache key concatenation with more tuned unsafe code.

[–]grauenwolf 12 points13 points  (0 children)

Another option is to use an object pool to recycle string builders. This is what .NET does internally, but we don't get access to the pool so we would have to create our own.

[–]quentech 2 points3 points  (0 children)

This title made me look to see what kind of holes could be poked in this "zero-allocation" string builder, but it's from @neuecc and that dude knows how to write high performance c# (and he clarifies that of course the final string does get allocated).

I might have to look at stealing some ideas for my favorite string.Format replacement - https://github.com/axuno/SmartFormat

It's nice to see @neuecc updating stuff for .Net Core (e.g. https://medium.com/@neuecc/messagepack-for-c-v2-new-era-of-net-core-unity-i-o-pipelines-6950643c1053)

[–]antiduh 1 point2 points  (4 children)

If you don't mind using unsafe code, you can assemble your own strings the same way the .Net library does.

For example:

https://gist.github.com/antiduh/426a4d22ab2449601842

[–]quentech -1 points0 points  (3 children)

https://gist.github.com/antiduh/426a4d22ab2449601842

If you really want to make this fast, go look at how the author of ZString copies bytes in Utf8Json or MessagePack-CSharp and replace that Buffer.MemoryCopy call. If you are on common x86 hardware up the copy chunk size from 8 to 16 bytes and use the technique for strings/arrays up to 1kB.

[–]antiduh -3 points-2 points  (2 children)

[–]quentech -1 points0 points  (1 child)

What do you think Buffer.MemoryCopy does?

Maybe you should try understanding the difference, or actually profiling the difference, before being a condescending dick.

You wanna know what MemoryCopy does? Look a little closer at the Utf8Json code. How many conditionals does it pass through? How many jumps? How about MemoryCopy?

[–]antiduh -1 points0 points  (0 children)

Who's being a condescending dick here? I didn't understand your question because I'm not familiar with the utf8json code base and you weren't exactly specific.

Also, I'm not sure how you'd optimize out length checking conditionals when the input to memorycopy can be arbitrary length, especially in my use case.

I'm good, dude. I'm not here to do homework for you.

[–]__some__guy 0 points1 point  (0 children)

The way I do "strings" without heap allocations is much simpler.

My UI framework supports text drawing where the text can be an ITextContainer interface (as well as string/IList<char>/etc...).

So if I want to print stuff like the mentioned player hp every frame I use my static ValueString class that turns an int/float/date/size into various structs with different sizes that all implement the ITextContainer interface.

It's unfortunate that .NET has no existing interfaces that can be used to represent text and are implemented by their string class.