Post | Sanctuary
top of page
  • Join our Discord!
  • Join our Kickstarter!

Trabalhando com o burst Working with Burst

Esse será um blog pequeno e também incrivelmente técnico, comparado com os outros que fiz.


Dessa vez, compartilharei minha experiência com o AVX no Burst. Pode ser super útil para desenvolvedores.


Como tudo funciona no mundo do Burst? Vetorizando um loop simples


Isso deveria ser simples, certo?

Bom, aparentemente não.

Vamos dar uma olhada num exemplo simples :

[BurstCompile] public struct VectorTest: IJob { public int size; public NativeArray<float> data; public void Execute() { for (int i = 0; i < size; i++) { data[i] += 10; } } }


Aqui está o código que ele expede. O código vetorizado está no ponteiro do mouse. 8 x float significa 8 wide como esperado do AVX 256.



Outro exemplo é com o SSe or streaming SIMD extensions. SIMD significando single instruction with multiple data. Como esperado, 128 bits wide, então 4x



Vamos tentar mudar para modo de metade de dados. Como a metade é metade do float, isso significa que deveria ser 2x a largura, ou 32, certo?



Hmm

Como você pode ver pelas fotos, não há 32. O que aconteceu? Como você pode ver, o avx2 nem o SSE suportam metades. Eles suportam curtos. Ou bytes. Ou dobros. O que ele faz agora é converter a metade para float e então adicionar para o modo lento e não vetorizado.


Enfim, vamos olhar outro exemplo.


[BurstCompile] public struct VectorTest : IJob { public int size; public NativeArray<float3> data; public void Execute() { for (int i = 0; i < size; i++) { data[i] *= 10; } } }


Eu apenas mudei Float para Float3, sem problemas, certo? Quer dizer, em tertmos de data, isso é exatamente como se eu fizesse os dados serem 3x maior.




Oh não. O que fizemos errado? Por que não está funcionando? Vamos tentar algumas coisas aleatórias. Vou pular para o chase e te dizer o que funciona.

[BurstCompile] public struct VectorTest : IJob { public int size; public NativeArray<float3> data; public void Execute() { for (int i = 0; i < 8; i++) { data[i] *= 10; } } }


O que mudou? Eu apenas mudei o tamanho para 8. Nada muito diferente, certo? Ele tecnicamente diz que está vetorizado agora, mas olhando o IR emitido, vemos que nenhuma instrução AVX está sendo usada. Agora podemos verificar isso ao mudar para SSE e vermos que nenhuma instrução será modificada



Na realidade, esta é a emissão final:



Como podemos ver, o que ele fez foi usar instruções SSE.


Aqui está o conjunto gerado




Como podemos ver, contém a instrução vaddps que você pode achar em https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=vaddps E ver que é uma extensão de vetor Mas sério, o que está ocorrendo aqui? Quando você usa o float2/3/4 o compilador gera instruções individuais para vetores. O seu código efetivamente se transforma em:


[BurstCompile] public struct VectorTest : IJob { public int size; public NativeArray<float3> data; public quaternion rotation; public void Execute() { for (int i = 0; i < 1; i++) { var innerData = data[i]; for (int j = 0; j < 3; j++) { innerData[i]+=10; } } } }


Então ele vetoriza o loop interno e diz que está feito. E o loop externo, quando ele fica muito grande, não consegue desenrolá-lo. Porque desenrolar é apenas copiar e colar o mesmo código. Mas com índices diferentes. O que beneficia a performance, mas não podemos fazê-lo infinitamente. O custo de carregar novas instruções fica maior do que a quantidade salva ao impedir o looping. Enfim, se você quiser vetorizar esse também, então apenas faça o loop interno e o loop externo desse jeito. Aqui um exemplo onde eu loopei unidades em um modo vetorizado

var indexOffset = 0; for (var i = 0; i < (unitCount + vectorSize - 1) / vectorSize; i++) { //this loop is vectorized for (var index = 0; index < vectorSize; index++) { var realIndex = index + indexOffset; var slot = freeSlots[realIndex]; var result = slot * spacing; result = math.mul(rotation, result); result += futureSight; result += formationCenter; tempMem[index] = result; } for (int index = 0; index < vectorSize; index++) { var result = tempMem[index]; var realIndex = index + indexOffset; if (realIndex >= unitCount) break; var unit = units[realIndex]; formationOffsets[unit.id] = result; Assert.IsTrue(math.isfinite(formationOffsets[unit.id]).All()); } indexOffset += 8; }

Como pode ver, no loop vetorizado eu fiz uma computação demorada, principalmente o quaternion com multiplicação float3. O que pode ser decomposto grosseiramente (Não é o mesmo exemplo)




Como pode-se ver ele spama multiplicações float 3, e honestamente você pode fazer um trabalho melhor do que esse.




Um pouco de vetorização manual Some manual vectorization

Eu escrevi esse código antes de descobrir como domar o Unityt auto-vectorizer, então foi tudo feito a mão. O que ele faz é simplesmente calcular a distância entre pontos no modo AVX.


O que estamos tentando realizar é implementar essas funções

public static float dot(float2 x, float2 y) { return x.x * y.x + x.y * y.y; }


public static float lengthsq(float2 x) { return dot(x, x); }


public static float distancesq(float2 x, float2 y) { return lengthsq(y - x); }



var cellUnitPosVector = X86.Avx.mm256_load_ps(cellPositionsPtr + unitInCellIndex); //contains 4 unit positions //vectorPosition is just a vector 2 that is copy pasted 4 times var offsetCellPositions = X86.Avx.mm256_sub_ps(cellUnitPosVector, vectorPosition); //cellUnitPosVector - vectorPosition var dotMul = X86.Avx.mm256_mul_ps(offsetCellPositions, offsetCellPositions); //x.x * y.x and x.y * y.y, we can abuse the fact that both x and y are the same // x1 y1 x2 y2... // * // x1 y1 x2 y2... var dotAdd = X86.Avx.mm256_hadd_ps(dotMul, dotMul); //This results in a vector where each second value is the same as the previous. We need to convert it to v128 by discarding every second result if (sqrtDistance) { dotAdd = X86.Avx.mm256_sqrt_ps(dotAdd); } result = new float4 { [0] = dotAdd.Float0, [1] = dotAdd.Float1, [2] = dotAdd.Float4, [3] = dotAdd.Float5 };


Como pode-se ver, todas as iguarias do AVX estão contidas no x86.Avx namespace. Basicamente carregamos 4 vezes o mesmo float2. Ex: [1,2,1,2,1,2,1,2] Faça o y-x, com o mm256_sub_ps Então na função dot, precisamos fazer: x.x*x.x, x.y*x.y Porém também precisamos da parte +. Então o que temos que fazer é somar todos os pares, e o resultado será 4 float wide. Vendo o website de intel podemos ver a instrução mm256_hadd_ps.


Apenas divida os valores nos colchetes por 32 Como você pode ver, ele adiciona à:

dst[0] = a[1] + a[0]

dst[1] = a[3] + a[2]

...

dst[4] = a[5] + a[4]

dst[5] = a[7] + a[6]


E nós manualmente extraímos todos os valores. Felizmente, isto é rápido porque o trabalho pesado é feito pelas instruções AVX.


Antes do fim, nós também checamos se precisamos calcular a raiz quadrada deles. Pode ser um pouco ineficiente, já que precisamos da raiz quadrada de 4 e de 8, mas fazendo dessa forma, pode ser necessário mais trabalho antes de estar realmente pronto, para preparar os dados.


Se você tiver qualquer pergunta sobre isso, então me pergunte no Discord.


Agradeço a todos que leram, Badump

3 views0 comments
bottom of page