Excerpts From the RavenDB Performance Team Report: Etags and Evil Code, Part II
Join the DZone community and get the full member experience.
Join For FreeIn my previous post, I talked about how we improved the performance of Etag parsing from 5 etags/ms to 3,500 etags/ms. In this post, I want to talk about the exact opposite problem, how we take an Etag and turn it into a string. Here is the original code that we had:
public unsafe override string ToString() { var sb = new StringBuilder(36); var buffer = stackalloc byte[8]; *((long*)buffer) = restarts; sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[7]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[6]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[5]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[4]]); sb.Append('-'); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[3]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[2]]); sb.Append('-'); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[1]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[0]]); sb.Append('-'); *((long*)buffer) = changes; sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[7]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[6]]); sb.Append('-'); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[5]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[4]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[3]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[2]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[1]]); sb.Append(GenericUtil.ByteToHexAsStringLookup[buffer[0]]); var etagAsString = sb.ToString(); Debug.Assert(etagAsString.Length == 36); //prevent stupid bugs if something is refactored return etagAsString; }
As you can see, we already optimized this a bit. It is using a string builder, it is using a lookup table to avoid costly byte to string. Note also that we use a stackalloc value, so there isn’t an actual allocation, but we are able to copy the values once, and then just directly access it. Which is cheaper than trying to do a lot of bit shifting.
So far so good. Running on 10 million Etags, this completes in 8.9 seconds. That is good, this gives us 1,125 Etags per milliseconds.
Here is the optimized version:
public unsafe override string ToString() { var results = new string('-', 36); fixed (char* buf = results) { var buffer = stackalloc byte[8]; *((long*)buffer) = restarts; var duget = GenericUtil.ByteToHexAsStringLookup[buffer[7]]; buf[0] = duget[0]; buf[1] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[6]]; buf[2] = duget[0]; buf[3] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[5]]; buf[4] = duget[0]; buf[5] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[4]]; buf[6] = duget[0]; buf[7] = duget[1]; //buf[8] = '-'; duget = GenericUtil.ByteToHexAsStringLookup[buffer[3]]; buf[9] = duget[0]; buf[10] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[2]]; buf[11] = duget[0]; buf[12] = duget[1]; //buf[13] = '-'; duget = GenericUtil.ByteToHexAsStringLookup[buffer[1]]; buf[14] = duget[0]; buf[15] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[0]]; buf[16] = duget[0]; buf[17] = duget[1]; //buf[18] = '-'; *((long*)buffer) = changes; duget = GenericUtil.ByteToHexAsStringLookup[buffer[7]]; buf[19] = duget[0]; buf[20] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[6]]; buf[21] = duget[0]; buf[22] = duget[1]; //buf[23] = '-'; duget = GenericUtil.ByteToHexAsStringLookup[buffer[5]]; buf[24] = duget[0]; buf[25] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[4]]; buf[26] = duget[0]; buf[27] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[3]]; buf[28] = duget[0]; buf[29] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[2]]; buf[30] = duget[0]; buf[31] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[1]]; buf[32] = duget[0]; buf[33] = duget[1]; duget = GenericUtil.ByteToHexAsStringLookup[buffer[0]]; buf[34] = duget[0]; buf[35] = duget[1]; return results; } }
Note that here we don’t bother with a string builder, we directly manipulate the string. And we still use all the other tricks (the lookup table, the no allocation, the works). This code managed to get to 5.5 seconds for 10,000,000 etags, or roughly 1,800 etags per millisecond. Roughly 37.5% improvement to a pretty important piece of code.
Do you see anything else that we can do to reduce the cost even further?
Published at DZone with permission of Oren Eini, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
The Dark Side of DevSecOps and Why We Need Governance Engineering
-
13 Impressive Ways To Improve the Developer’s Experience by Using AI
-
What to Pay Attention to as Automation Upends the Developer Experience
-
Unlocking Game Development: A Review of ‘Learning C# By Developing Games With Unity'
Comments