Fiquei surpreso ao ver que, ao contrário do Vector256, o ExtractMostSignificantBits do Vector64 não usa um intrínseco para fazer seu trabalho. Usar o intrínseco transforma estas 36 linhas:
mov qword ptr [rsp+0x80], r12
movzx rdx, byte ptr [rsp+0x80]
shr edx, 7
movzx r8, byte ptr [rsp+0x81]
shr r8d, 7
add r8d, r8d
or edx, r8d
movzx r8, byte ptr [rsp+0x82]
shr r8d, 7
shl r8d, 2
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x83]
shr r8d, 7
shl r8d, 3
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x84]
shr r8d, 7
shl r8d, 4
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x85]
shr r8d, 7
shl r8d, 5
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x86]
shr r8d, 7
shl r8d, 6
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x87]
shr r8d, 7
shl r8d, 7
or r8d, edx
Nestes 4:
mov qword ptr [rsp+0x70], rcx
mov r10, qword ptr [rsp+0x70]
mov r15, 0x8080808080808080
pext r10, r10, r15
Parece que você poderia implementá-lo no Vector64.cs com algo assim:
public static uint ExtractMostSignificantBits<T>(this Vector64<T> vector)
{
if (Bmi2.X64.IsSupported)
{
ulong mask = 0;
if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte))
mask = 0x8080808080808080;
else if (typeof(T) == typeof(short) || typeof(T) == typeof(ushort))
mask = 0x8000800080008000;
else if (typeof(T) == typeof(int) || typeof(T) == typeof(uint) || typeof(T) == typeof(float))
mask = 0x8000000080000000;
else if (typeof(T) == typeof(long) || typeof(T) == typeof(ulong) || typeof(T) == typeof(double))
mask = 0x8000000000000000;
else if (typeof(T) == typeof(nint) || typeof(T) == typeof(nuint))
if (IntPtr.Size == 4)
mask = 0x8000000080000000;
else
mask = 0x8000000000000000;
ulong u = vector._00;
ulong retval = Bmi2.X64.ParallelBitExtract(u, mask);
return (uint)retval;
}
// Fall back to the old code
uint result = 0;
for (int index = 0; index < Vector64<T>.Count; index++)
{
uint value = Scalar<T>.ExtractMostSignificantBit(vector.GetElementUnsafe(index));
result |= (value << index);
}
return result;
}
Não é como se o BMI2 fosse mais "novo", tendo sido introduzido por volta de 2013. Além disso, considerando a compilação JIT, não há muito custo em fazê-lo dessa maneira.
Eu escrevi um código de teste (disponível mediante solicitação) e ele produz os mesmos resultados que o código existente, apenas um pouco mais que o dobro da velocidade (possivelmente ainda mais rápido com algumas melhorias no Vector64.GetElementUnsafe).
Há alguma nuance ou caso especial aqui que eu tenha esquecido e que torna o uso do intrínseco inaceitável? Ou alguém simplesmente esqueceu disso?
Então, para finalizar, há (pelo menos) 3 razões pelas quais minha solução proposta não é a escolhida pela equipe .Net ao escrever
Vector64.ExtractMostSignificantBits
:A família BMI2 apresenta um desempenho INCRIVELMENTE ruim nas famílias de processadores Excavator e Zen1/Zen2. Embora adicionar o código que propus melhore o desempenho da minha máquina, é preciso evitar, se possível, piorar a situação para outras pessoas.
A família BMI2 é mais recente que a família SSE, o que significa que o uso de instruções SSE não só evita o problema do Zen1/Zen2, como também permite o suporte a máquinas ainda mais antigas. O BMI2 foi introduzido em 2013, enquanto o SSE2 remonta a 2000.
CPUs de baixo custo (Celeron/Pentium) e Intel de baixo consumo (família Silvermont) só obtiveram suporte ao BMI2 significativamente mais tarde, com os Pentium/Celeron Ice Lake e Gracemont (núcleos E Alder Lake), respectivamente. CPUs derivadas do Skylake e anteriores da marca Pentium/Celeron tinham o AVX1/2 desabilitado, assim como o BMI1/2, talvez pela desativação da decodificação de prefixos VEX.
As instruções SSE produzem código mais rápido que as BMI2, mesmo trabalhando em mais bits.
Olhando para os números:
Então, como todos previram, usar instruções vetoriais de 128 bits é o claro vencedor.
Agora é hora de aplicar o que aprendi e mostrar isso ao pessoal do .Net para ver se eles estão interessados.