I noticed the same thing. Modifying the code to make a simple cast made the code run from 5.6353 sec to 2.3411 sec, but this benchmark still shows very little about optimizations.
That may not even be the biggest flaw; using DateTime.Now as the timing mechanism for small benchmarks is pretty inaccurate because the resolution is rather poor.
The recommended way is to use System.Diagnostics.Stopwatch, since that uses the CPU's built-in high-performance timer (HPET).
Why use strings at all? Guid.NewGuid() is built in, and way faster. Given that the C# code is almost willfully unoptimized, I'm not sure what the benchmark was meant to prove.
To be fair, it was trying to compare JIT vs. non-JIT. As futile as that me be via a microbenchmark like this, you need to compare as similar an execution path as possible. As it happens, the C# version is doing 16000000 more lookups for the Encoding than the RealBASIC version. Remove that, and C# - and by the premise of the post, JITters - wins.
It looks like the RealBASIC version was written first and then translated straight into C#.
In reality, you'd not create the encoding each time, and either use a StringBuilder to build up the string, or just use Encoding.GetString.
C# on my machine:
As per blog: 50s
Creating Encoding once: 24s
Using StringBuilder: 11s
Using Encoding.GetString: 2.5s
Use Random.NextBytes: 2s
RealBASIC on my machine:
As per blog: 27s
Interestingly despite advice I'd found on the internet[1] using array joining didn't really make the RealBASIC version any faster, so from what I can tell it's generally as fast as it could be using the out of the box libraries.
(My machine isn't exactly idle and ideal for microbenchmarking atm, but the differences are large enough for that not to matter)
This doesn't really provide a good evaluation on the state of JITs.
I'm fairly surprised in the results using StringBuilder. I didn't know it was so fast compared with plain concatenation. According to what I've read in MSDN [1], memory allocation has a lot to do with it. I think I'll start using this class in my code.
By the way, this leads me to one question: does the code
string a = "a" + "b" + "c" + "d";
generate one or three memory allocations (one to compute each concatenation)?
Because you're concatenating literals, the compiler will translate it into string a = "abcd" ;)
Strings are outwardly immutable, so concatenating any two strings will create an entirely new String object, along with the memory allocation needed for that. However, there are some internal methods that allow String and other classes in the same corelib assembly (such as StringBuilder) to mutate them.
`string a = x + y + z` will be translated into `string a = String.Concat(x, y, z)`. Because it knows the final length it can use the internal String methods to allocate a new String long enough to fit all the arguments and then copy the characters into that without any intermediate representations. [1]
In a loop it doesn't know how big the final result will be, and so has to create a new String object on each iteration. If you're doing 16000000 concatenations as in this test that's a lot of intermediate strings that are largely being wasted.
StringBuilders have a private String object and use the internal String methods to mutate that when you call Append. When you call ToString it returns a copy of the private String object. Again, this avoids creating lots of new intermediate String objects, but does have the overhead of having to guess how large the initial String should be, and growing it if you append too much (similar to how Lists work).
[1] I believe in Java it's translated into using StringBuffer or StringBuilder, which has a small overhead of creating the StringBuffer/StringBuilder object.
[Edit: Corrected how String.Concat and StringBuilders work based on the Mono codebase]
What I found the most interesting was how quickly Miguel de Icaza was to post on the message, trying to get involved with helping improve the performance. I'm not familiar enough with the original poster, nor with opensim, so I don't know of this specific developer is someone the whole Mono community is keeping an eye on. Regardless I find it quite encouraging that the main public face of Mono is always keeping an eye out to help the community.
It's a PR thing more than an actually-helping thing. For example, the OpenSim developers had a problem where memory use exploded rapidly on heavily-loaded regions running under Mono, to the point that they had uptimes measured in single-digit hours, yet the same regions running the same code and workload under .Net were just fine. Miguel de Icaza's response was to run a totally idle region for 24 hours under both Mono and .Net, produce pretty graphs showing that Mono used less memory, and claim that the problem didn't exist. The developers pointed out the flaws in his tests, but he never corrected his statement or helped to find the actual problem.
I'm trying to figure out what piece of the puzzle I'm missing. I converted the script to java, using apache's RandomStringUtils.randomAscii, and I get:
With StringBuilder: 2.61 seconds
With String Concat: 3.03 seconds
I have to assume there's another loop running these, but I'm not seeing it. Each dictionary has 1 million elements at the end.
I just implemented it inside a junit test, here's the class:
The C# version has the overhead of looking up the Encoding and creating a byte array each character. This can be avoided by using the libraries correctly.
Strangely your StringBuilder version seems to really slow down for me at around iteration 6000000. If I replace the repeated use of uuid.toString with `String uuidStr = uuid.toString()` it works fine. Looks like the performance of toString differs between versions.
18 comments
[ 2.6 ms ] story [ 63.4 ms ] threadThe RealBASIC version calls the inbuilt "chr" function while in C# it calls:
I'm sure there's some overhead to calling this 16 million times.The recommended way is to use System.Diagnostics.Stopwatch, since that uses the CPU's built-in high-performance timer (HPET).
In reality, you'd not create the encoding each time, and either use a StringBuilder to build up the string, or just use Encoding.GetString.
C# on my machine:
As per blog: 50s
Creating Encoding once: 24s
Using StringBuilder: 11s
Using Encoding.GetString: 2.5s
Use Random.NextBytes: 2s
RealBASIC on my machine:
As per blog: 27s
Interestingly despite advice I'd found on the internet[1] using array joining didn't really make the RealBASIC version any faster, so from what I can tell it's generally as fast as it could be using the out of the box libraries.
(My machine isn't exactly idle and ideal for microbenchmarking atm, but the differences are large enough for that not to matter)
This doesn't really provide a good evaluation on the state of JITs.
[1] http://forums.realsoftware.com/viewtopic.php?f=13&t=2510...
[Edit: Added RealBASIC times]
By the way, this leads me to one question: does the code
generate one or three memory allocations (one to compute each concatenation)?Strings are outwardly immutable, so concatenating any two strings will create an entirely new String object, along with the memory allocation needed for that. However, there are some internal methods that allow String and other classes in the same corelib assembly (such as StringBuilder) to mutate them.
`string a = x + y + z` will be translated into `string a = String.Concat(x, y, z)`. Because it knows the final length it can use the internal String methods to allocate a new String long enough to fit all the arguments and then copy the characters into that without any intermediate representations. [1]
In a loop it doesn't know how big the final result will be, and so has to create a new String object on each iteration. If you're doing 16000000 concatenations as in this test that's a lot of intermediate strings that are largely being wasted.
StringBuilders have a private String object and use the internal String methods to mutate that when you call Append. When you call ToString it returns a copy of the private String object. Again, this avoids creating lots of new intermediate String objects, but does have the overhead of having to guess how large the initial String should be, and growing it if you append too much (similar to how Lists work).
[1] I believe in Java it's translated into using StringBuffer or StringBuilder, which has a small overhead of creating the StringBuffer/StringBuilder object.
[Edit: Corrected how String.Concat and StringBuilders work based on the Mono codebase]
Note that the actual magic consists of internal methods in String.cs:
- FormatHelper https://github.com/mono/mono/blob/master/mcs/class/corlib/Sy...
- InternalSetChar https://github.com/mono/mono/blob/master/mcs/class/corlib/Sy...
- CharCopy https://github.com/mono/mono/blob/master/mcs/class/corlib/Sy...
With StringBuilder: 2.61 seconds With String Concat: 3.03 seconds
I have to assume there's another loop running these, but I'm not seeing it. Each dictionary has 1 million elements at the end.
I just implemented it inside a junit test, here's the class:
http://pastebin.com/sFZjA1p2
Turns out java performance isn't bad at all.
EDIT:
I'm a dumbass, was converting nanos to full seconds. Post has been edited.