I've implemented a method for parsing an unsigned integer string of length <= 8 using SIMD intrinsics available in .NET as follows:
public unsafe static uint ParseUint(string text)
{
fixed (char* c = text)
{
var parsed = Sse3.LoadDquVector128((byte*) c);
var shift = (8 - text.Length) * 2;
var shifted = Sse2.ShiftLeftLogical128BitLane(parsed,
(byte) (shift));
Vector128<byte> digit0 = Vector128.Create((byte) '0');
var reduced = Sse2.SubtractSaturate(shifted, digit0);
var shortMult = Vector128.Create(10, 1, 10, 1, 10, 1, 10, 1);
var collapsed2 = Sse2.MultiplyAddAdjacent(reduced.As<byte, short>(), shortMult);
var repack = Sse41.PackUnsignedSaturate(collapsed2, collapsed2);
var intMult = Vector128.Create((short)0, 0, 0, 0, 100, 1, 100, 1);
var collapsed3 = Sse2.MultiplyAddAdjacent(repack.As<ushort,short>(), intMult);
var e1 = collapsed3.GetElement(2);
var e2 = collapsed3.GetElement(3);
return (uint) (e1 * 10000 + e2);
}
}
Sadly, a comparison with a baseline uint.Parse() gives the following, rather unimpressive, result:
| Method | Mean | Error | StdDev |
|---|---|---|---|
| Baseline | 15.157 ns | 0.0325 ns | 0.0304 ns |
| ParseSimd | 3.269 ns | 0.0115 ns | 0.0102 ns |
What are some of the ways the above code can be improved? My particular areas of concern are:
text.LengthMultiplyAddAdjacent involving a vector of 0s and 1~~GetElement() -- maybe there's some ToScalar() call that can happen somwehere?First of all, 5x improvement is not “rather unimpressive”.
I would not do the last step with scalar code, here’s an alternative:
// _mm_shuffle_epi32( x, _MM_SHUFFLE( 3, 3, 2, 2 ) )
collapsed3 = Sse2.Shuffle( collapsed3, 0xFA );
// _mm_mul_epu32
var collapsed4 = Sse2.Multiply( collapsed3.As<int, uint>(), Vector128.Create( 10000u, 0, 1, 0 ) ).As<ulong, uint>();
// _mm_add_epi32( x, _mm_srli_si128( x, 8 ) )
collapsed4 = Sse2.Add( collapsed4, Sse2.ShiftRightLogical128BitLane( collapsed4, 8 ) );
return collapsed4.GetElement( 0 );
The C++ version gonna be way faster than what happens on my PC (.NET Core 3.1). The generated code is not good. They initialize constants like this:
00007FFAD10B11B6 xor ecx,ecx
00007FFAD10B11B8 mov dword ptr [rsp+20h],ecx
00007FFAD10B11BC mov dword ptr [rsp+28h],64h
00007FFAD10B11C4 mov dword ptr [rsp+30h],1
00007FFAD10B11CC mov dword ptr [rsp+38h],64h
00007FFAD10B11D4 mov dword ptr [rsp+40h],1
They use stack memory instead of another vector register. It looks like JIT developers forgot there’re 16 vector registers there, the complete function only uses xmm0.
00007FFAD10B1230 vmovapd xmmword ptr [rbp-0C0h],xmm0
00007FFAD10B1238 vmovapd xmm0,xmmword ptr [rbp-0C0h]
00007FFAD10B1240 vpsrldq xmm0,xmm0,8
00007FFAD10B1245 vpaddd xmm0,xmm0,xmmword ptr [rbp-0C0h]