Implementé un método para analizar una cadena de enteros sin signo de longitud <= 8 usando los intrínsecos SIMD disponibles en .NET de la siguiente manera:
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); } } Lamentablemente, una comparación con una línea de base uint.Parse() da el siguiente resultado, bastante poco impresionante:
| Método | Significar | Error | Desv.estándar |
|---|---|---|---|
| Base | 15.157 ns | 0,0325 ns | 0,0304 ns |
| ParseSimd | 3.269 ns | 0,0115 ns | 0,0102 ns |
¿Cuáles son algunas de las formas en que se puede mejorar el código anterior? Mis áreas particulares de preocupación son:
text.LengthMultiplyAddAdjacent que involucra un vector de 0 s y 1 ~~GetElement() -- ¿quizás haya alguna llamada a ToScalar() que pueda ocurrir en algún lugar?En primer lugar, la mejora de 5x no es "bastante poco impresionante".
No haría el último paso con código escalar, aquí hay una alternativa:
// _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 );La versión C++ será mucho más rápida que lo que sucede en mi PC (.NET Core 3.1). El código generado no es bueno. Ellos inicializan constantes como esta:
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 Usan memoria de pila en lugar de otro registro vectorial. Parece que los desarrolladores de JIT olvidaron que hay 16 registros vectoriales allí, la función completa solo usa 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]