Há _mm_div_ps para divisão de valores de ponto flutuante, há _mm_mullo_epi16 para multiplicação de inteiro. Mas existe algo para divisão inteira (valor de 16 bits)? Como posso conduzir essa divisão?
Por favor, veja a class vetorial de Agner Fog que ele implementou um algoritmo rápido para fazer a divisão inteira com SSE / AVX para palavras de 8 bits, 16 bits e 32 bits (mas não 64 bits) http://www.agner.org/ otimizar / # vectorclass
Olhe no arquivo vectori128.h para o código e uma descrição do algoirthm como seu manual bem escrito VectorClass.pdf
Aqui está um fragment descrevendo o algoritmo de seu manual.
“Integer division Não há instruções no conjunto de instruções x86 e suas extensões que são úteis para a divisão de vetores inteiros, e tais instruções seriam bastante lentas se elas existissem. Portanto, a biblioteca de classs de vetor está usando um algoritmo para divisão de inteiros rápidos. O princípio básico deste algoritmo pode ser expresso nesta fórmula: a / b ≈ a * (2n / b) >> n Este cálculo segue os seguintes passos: 1. encontre um valor adequado para n 2. calcule 2n / b 3. calcular correções necessárias para erros de arredondamento 4. fazer a multiplicação e shift-right e aplicar correções para erros de arredondamento
Esta fórmula é vantajosa se vários números forem divididos pelo mesmo divisor b. Os passos 1, 2 e 3 só precisam ser feitos uma vez enquanto o passo 4 é repetido para cada valor do dividendo a. Os detalhes matemáticos são descritos no arquivo vectori128.h. (Veja também T. Granlund e PL Montgomery: Divisão por inteiros invariantes usando a multiplicação, continuações do SIGPLAN. “…
Edit: perto do final do arquivo vectori128.h mostra como fazer uma divisão curta com uma variável escalar “Leva mais tempo para calcular os parâmetros usados para divisão rápida do que para fazer a divisão. Portanto, é vantajoso usar o mesmo divisor Por exemplo, para dividir 80 inteiros curtos não assinados por 10:
short x = 10; uint16_t dividends[80], quotients[80]; // numbers to work with Divisor_us div10(x); // make divisor object for dividing by 10 Vec8us temp; // temporary vector for (int i = 0; i < 80; i += 8) { // loop for 4 elements per iteration temp.load(dividends+i); // load 4 elements temp /= div10; // divide each element by 10 temp.store(quotients+i); // store 4 elements }
"
Editar: divisão inteira por um vetor de calções
#include #include "vectorclass.h" int main() { short numa[] = {10, 20, 30, 40, 50, 60, 70, 80}; short dena[] = {10, 20, 30, 40, 50, 60, 70, 80}; Vec8s num = Vec8s().load(numa); Vec8s den = Vec8s().load(dena); Vec4f num_low = to_float(extend_low(num)); Vec4f num_high = to_float(extend_high(num)); Vec4f den_low = to_float(extend_low(den)); Vec4f den_high = to_float(extend_high(den)); Vec4f qf_low = num_low/den_low; Vec4f qf_high = num_high/den_high; Vec4i q_low = truncate_to_int(qf_low); Vec4i q_high = truncate_to_int(qf_high); Vec8s q = compress(q_low, q_high); for(int i=0; i<8; i++) { printf("%d ", q[i]); } printf("\n"); }
isto é para recém-chegados graças à solução original: a biblioteca de sub-rotinas da Agner Fog fez a mágica comigo na otimização
aqui está a situação quando você divide no mesmo valor de variável várias vezes (como no loop grande)
#include unsigned int a, b, d; unsigned int divisor = any_random_value; div_u32 OptimumDivision(divisor); a/OptimumDivision; b/OptimumDivision;
isso é para unsigned int – se você precisar de valor negativo use div_i32
vez disso, o que foi mais rápido em meus testes, mesmo que o manual diga o contrário
Eu tenho cerca de 3x desempenho ou mais
O método Agner Fog ( http://www.agner.org/optimize/#vectorclass ) funciona muito bem se a divisão é feita com um único divisor. Além disso, esse método tem ainda mais benefícios se o divisor for conhecido em tempo de compilation ou se não mudar com frequência em tempo de execução.
No entanto, ao executar a divisão SIMD em elementos __m128i
modo que nenhuma informação esteja disponível para o divisor e o dividendo no momento da compilation, não temos opção, mas para converter em float e executar o cálculo. Por outro lado, usar _mm_div_ps
não nos proporcionará melhorias de velocidade incríveis, já que essa instrução tem latência variável de 11 a 14 ciclos na maioria das micro-arquiteturas e às vezes pode ir até 38 ciclos se considerarmos o Knights Landing. Além disso, essa instrução não é totalmente canalizada e tem taxa de transferência recíproca de 3-6 ciclos, dependendo da microarquitetura.
No entanto, podemos evitar _mm_div_ps
e usar _mm_rcp_ss
.
Infelizmente __m128 _mm_rcp_ss (__m128 a)
é rápido apenas porque fornece aproximação. A saber (retirado do Guia Intel Intrnisics ):
Calcule a recíproca aproximada do elemento de ponto flutuante de precisão única (32 bits) inferior em a, armazene o resultado no elemento inferior de dst e copie os 3 elementos compactados superiores de um para os elementos superiores de dst. O erro relativo máximo para essa aproximação é menor que 1,5 * 2 ^ -12.
Portanto, para se beneficiar de _mm_rcp_ss
, precisamos compensar a perda devido à aproximação. Há um grande trabalho feito nessa direção, disponível em Divisão aprimorada por inteiros invariantes de Niels Möller e Torbjörn Granlund :
Devido à falta de instruções de divisão eficientes nos processadores atuais, a divisão é executada como uma multiplicação usando uma aproximação de única palavra pré-computada da recíproca do divisor, seguida por um par de etapas de ajuste.
Para calcular a divisão inteira assinada de 16 bits, precisamos apenas de uma etapa de ajuste e baseamos nossa solução adequadamente.
static inline __m128i _mm_div_epi16(const __m128i &a_epi16, const __m128i &b_epi16) { // // Setup the constants. // const __m128 two = _mm_set1_ps(2.00000051757f); const __m128i lo_mask = _mm_set1_epi32(0xFFFF); // // Convert to two 32-bit integers // const __m128i a_hi_epi32 = _mm_srai_epi32(a_epi16, 16); const __m128i a_lo_epi32_shift = _mm_slli_epi32(a_epi16, 16); const __m128i a_lo_epi32 = _mm_srai_epi32(a_lo_epi32_shift, 16); const __m128i b_hi_epi32 = _mm_srai_epi32(b_epi16, 16); const __m128i b_lo_epi32_shift = _mm_slli_epi32(b_epi16, 16); const __m128i b_lo_epi32 = _mm_srai_epi32(b_lo_epi32_shift, 16); // // Convert to 32-bit floats // const __m128 a_hi = _mm_cvtepi32_ps(a_hi_epi32); const __m128 a_lo = _mm_cvtepi32_ps(a_lo_epi32); const __m128 b_hi = _mm_cvtepi32_ps(b_hi_epi32); const __m128 b_lo = _mm_cvtepi32_ps(b_lo_epi32); // // Calculate the reciprocal // const __m128 b_hi_rcp = _mm_rcp_ps(b_hi); const __m128 b_lo_rcp = _mm_rcp_ps(b_lo); // // Calculate the inverse // #ifdef __FMA__ const __m128 b_hi_inv_1 = _mm_fnmadd_ps(b_hi_rcp, b_hi, two); const __m128 b_lo_inv_1 = _mm_fnmadd_ps(b_lo_rcp, b_lo, two); #else const __m128 b_mul_hi = _mm_mul_ps(b_hi_rcp, b_hi); const __m128 b_mul_lo = _mm_mul_ps(b_lo_rcp, b_lo); const __m128 b_hi_inv_1 = _mm_sub_ps(two, b_mul_hi); const __m128 b_lo_inv_1 = _mm_sub_ps(two, b_mul_lo); #endif // // Compensate for the loss // const __m128 b_hi_rcp_1 = _mm_mul_ps(b_hi_rcp, b_hi_inv_1); const __m128 b_lo_rcp_1 = _mm_mul_ps(b_lo_rcp, b_lo_inv_1); // // Perform the division by multiplication // const __m128 hi = _mm_mul_ps(a_hi, b_hi_rcp_1); const __m128 lo = _mm_mul_ps(a_lo, b_lo_rcp_1); // // Convert back to integers // const __m128i hi_epi32 = _mm_cvttps_epi32(hi); const __m128i lo_epi32 = _mm_cvttps_epi32(lo); // // Zero-out the unnecessary parts // const __m128i hi_epi32_shift = _mm_slli_epi32(hi_epi32, 16); #ifdef __SSE4_1__ // // Blend the bits, and return // return _mm_blend_epi16(lo_epi32, hi_epi32_shift, 0xAA); #else // // Blend the bits, and return // const __m128i lo_epi32_mask = _mm_and_si128(lo_epi32, const_mm_div_epi16_lo_mask); return _mm_or_si128(hi_epi32_shift, lo_epi32_mask); #endif }
Esta solução pode funcionar apenas com SSE2
e utilizará o FMA
se disponível. No entanto, pode ser que usar divisão simples seja tão rápido (ou até mais rápido) quanto usar a aproximação.
Na presença do AVX
esta solução pode ser melhorada, pois as partes alta e baixa podem ser processadas ao mesmo tempo usando um registro AVX
.
Validação
Como estamos lidando apenas com 16 bits, podemos facilmente validar a correção da solução em poucos segundos usando o teste de força bruta:
void print_epi16(__m128i a) { int i; int16_t tmp[8]; _mm_storeu_si128( (__m128i*) tmp, a); for (i = 0; i < 8; i += 1) { printf("%8d ", (int) tmp[i]); } printf("\n"); } bool run_mm_div_epi16(const int16_t *a, const int16_t *b) { const size_t n = 8; int16_t result_expected[n]; int16_t result_obtained[n]; // // Derive the expected result // for (size_t i = 0; i < n; i += 1) { result_expected[i] = a[i] / b[i]; } // // Now perform the computation // const __m128i va = _mm_loadu_si128((__m128i *) a); const __m128i vb = _mm_loadu_si128((__m128i *) b); const __m128i vr = _mm_div_epi16(va, vb); _mm_storeu_si128((__m128i *) result_obtained, vr); // // Check for array equality // bool eq = std::equal(result_obtained, result_obtained + n, result_expected); if (!eq) { cout << "Testing of _mm_div_epi16 failed" << endl << endl; cout << "a: "; print_epi16(va); cout << "b: "; print_epi16(vb); cout << endl; cout << "results_obtained: "; print_epi16(vr); cout << "results_expected: "; print_epi16(_mm_loadu_si128((__m128i *) result_expected)); cout << endl; } return eq; } void test_mm_div_epi16() { const int n = 8; bool correct = true; // // Brute-force testing // int16_t a[n]; int16_t b[n]; for (int32_t i = INT16_MIN; correct && i <= INT16_MAX; i += n) { for (int32_t j = 0; j < n; j += 1) { a[j] = (int16_t) (i + j); } for (int32_t j = INT16_MIN; correct && j < 0; j += 1) { const __m128i jv = _mm_set1_epi16((int16_t) j); _mm_storeu_si128((__m128i *) b, jv); correct = correct && run_mm_div_epi16(a, b); } for (int32_t j = 1; correct && j <= INT16_MAX; j += 1) { const __m128i jv = _mm_set1_epi16((int16_t) j); _mm_storeu_si128((__m128i *) b, jv); correct = correct && run_mm_div_epi16(a, b); } } if (correct) { cout << "Done!" << endl; } else { cout << "_mm_div_epi16 can not be validated" << endl; } }
Tendo a solução acima, a implementação do AVX2
é direta:
static inline __m256i _mm256_div_epi16(const __m256i &a_epi16, const __m256i &b_epi16) { // // Setup the constants. // const __m256 two = _mm256_set1_ps(2.00000051757f); // // Convert to two 32-bit integers // const __m256i a_hi_epi32 = _mm256_srai_epi32(a_epi16, 16); const __m256i a_lo_epi32_shift = _mm256_slli_epi32(a_epi16, 16); const __m256i a_lo_epi32 = _mm256_srai_epi32(a_lo_epi32_shift, 16); const __m256i b_hi_epi32 = _mm256_srai_epi32(b_epi16, 16); const __m256i b_lo_epi32_shift = _mm256_slli_epi32(b_epi16, 16); const __m256i b_lo_epi32 = _mm256_srai_epi32(b_lo_epi32_shift, 16); // // Convert to 32-bit floats // const __m256 a_hi = _mm256_cvtepi32_ps(a_hi_epi32); const __m256 a_lo = _mm256_cvtepi32_ps(a_lo_epi32); const __m256 b_hi = _mm256_cvtepi32_ps(b_hi_epi32); const __m256 b_lo = _mm256_cvtepi32_ps(b_lo_epi32); // // Calculate the reciprocal // const __m256 b_hi_rcp = _mm256_rcp_ps(b_hi); const __m256 b_lo_rcp = _mm256_rcp_ps(b_lo); // // Calculate the inverse // const __m256 b_hi_inv_1 = _mm256_fnmadd_ps(b_hi_rcp, b_hi, two); const __m256 b_lo_inv_1 = _mm256_fnmadd_ps(b_lo_rcp, b_lo, two); // // Compensate for the loss // const __m256 b_hi_rcp_1 = _mm256_mul_ps(b_hi_rcp, b_hi_inv_1); const __m256 b_lo_rcp_1 = _mm256_mul_ps(b_lo_rcp, b_lo_inv_1); // // Perform the division by multiplication // const __m256 hi = _mm256_mul_ps(a_hi, b_hi_rcp_1); const __m256 lo = _mm256_mul_ps(a_lo, b_lo_rcp_1); // // Convert back to integers // const __m256i hi_epi32 = _mm256_cvttps_epi32(hi); const __m256i lo_epi32 = _mm256_cvttps_epi32(lo); // // Blend the low and the high-parts // const __m256i hi_epi32_shift = _mm256_slli_epi32(hi_epi32, 16); return _mm256_blend_epi16(lo_epi32, hi_epi32_shift, 0xAA); }
Podemos usar o mesmo método descrito acima para realizar a validação do código.
Podemos avaliar o desempenho usando os flops de medida por ciclo (F / C). Neste cenário, gostamos de ver quantas divisões podemos realizar por ciclo. Para isso, definimos dois vetores b
e realizamos a divisão por pontos. Ambos b
são preenchidos com dados randoms usando xorshift32 , inicializados com uint32_t state = 3853970173;
Eu uso RDTSC
para medir os ciclos, realizando 15 repetições com cache quente e usando a mediana como resultado. Para evitar os efeitos de escala de frequência e compartilhamento de resources nas medições, o Turbo Boost e o Hyper-Threading estão desativados. Para rodar o código eu utilizo o Intel Xeon CPU E3-1285L v3
3.10GHz Haswell com 32GB de RAM e 25,6 GB / s de largura de banda para memory principal, rodando Debian GNU / Linux 8 (jessie), kernel 3.16.43-2+deb8u3
. gcc
usado é 4.9.2-10
. Os resultados são fornecidos abaixo:
Implementação Pure SSE2
Nós comparamos a divisão simples com o algoritmo proposto acima:
=============================================================== = Compiler & System info =============================================================== Current CPU : Intel(R) Xeon(R) CPU E3-1285L v3 @ 3.10GHz CXX Compiler ID : GNU CXX Compiler Path : /usr/bin/c++ CXX Compiler Version : 4.9.2 CXX Compiler Flags : -O3 -std=c++11 -msse2 -mno-fma -------------------------------------------------------------------------------- | Size | Division F/C | Division B/W | Approx. F/C | Approximation B/W | -------------------------------------------------------------------------------- | 128 | 0.5714286 | 26911.45 MB/s | 0.5019608 | 23634.21 MB/s | | 256 | 0.5714286 | 26909.17 MB/s | 0.5039370 | 23745.44 MB/s | | 512 | 0.5707915 | 26928.14 MB/s | 0.5039370 | 23763.79 MB/s | | 1024 | 0.5707915 | 26936.33 MB/s | 0.5039370 | 23776.85 MB/s | | 2048 | 0.5709507 | 26938.51 MB/s | 0.5039370 | 23780.25 MB/s | | 4096 | 0.5708711 | 26940.56 MB/s | 0.5039990 | 23782.65 MB/s | | 8192 | 0.5708711 | 26940.16 MB/s | 0.5039370 | 23781.85 MB/s | | 16384 | 0.5704735 | 26921.76 MB/s | 0.4954040 | 23379.24 MB/s | | 32768 | 0.5704537 | 26921.26 MB/s | 0.4954639 | 23382.13 MB/s | | 65536 | 0.5703147 | 26914.53 MB/s | 0.4943539 | 23330.13 MB/s | | 131072 | 0.5691680 | 26860.21 MB/s | 0.4929539 | 23264.40 MB/s | | 262144 | 0.5690618 | 26855.60 MB/s | 0.4929187 | 23262.22 MB/s | | 524288 | 0.5691378 | 26858.75 MB/s | 0.4929488 | 23263.56 MB/s | | 1048576 | 0.5677474 | 26794.14 MB/s | 0.4918968 | 23214.34 MB/s | | 2097152 | 0.5371243 | 25348.39 MB/s | 0.4700511 | 22183.07 MB/s | | 4194304 | 0.5128146 | 24200.82 MB/s | 0.4529809 | 21377.28 MB/s | | 8388608 | 0.5036971 | 23770.36 MB/s | 0.4438345 | 20945.84 MB/s | | 16777216 | 0.5005390 | 23621.14 MB/s | 0.4409909 | 20811.32 MB/s | | 33554432 | 0.4992792 | 23561.90 MB/s | 0.4399777 | 20763.49 MB/s | --------------------------------------------------------------------------------
Podemos observar como a divisão simples será ligeiramente mais rápida que a etapa de aproximação proposta. Neste cenário, podemos concluir que a utilização da aproximação SSE2
será sub-ótima, em uma microarquitetura Haswell.
No entanto, se executarmos os mesmos resultados em uma máquina Sandy Bridge mais antiga, como a Intel Xeon (R) X5680 @ 3.33GHz, já podemos ver o benefício da aproximação:
=============================================================== = Compiler & System info =============================================================== Current CPU : Intel(R) Xeon(R) CPU X5680 @ 3.33GHz CXX Compiler ID : GNU CXX Compiler Path : /usr/bin/c++ CXX Compiler Version : 4.8.5 CXX Compiler Flags : -O3 -std=c++11 -msse2 -mno-fma -------------------------------------------------------------------------------- | Size | Division F/C | Division B/W | Approx. F/C | Approximation B/W | -------------------------------------------------------------------------------- | 128 | 0.2857143 | 14511.41 MB/s | 0.3720930 | 18899.89 MB/s | | 256 | 0.2853958 | 14512.51 MB/s | 0.3715530 | 18898.91 MB/s | | 512 | 0.2853958 | 14510.53 MB/s | 0.3715530 | 18896.44 MB/s | | 1024 | 0.2853162 | 14511.81 MB/s | 0.3700759 | 18824.00 MB/s | | 2048 | 0.2853162 | 14511.04 MB/s | 0.3708130 | 18860.31 MB/s | | 4096 | 0.2852964 | 14511.16 MB/s | 0.3711826 | 18879.27 MB/s | | 8192 | 0.2852666 | 14510.23 MB/s | 0.3713172 | 18886.39 MB/s | | 16384 | 0.2852616 | 14509.86 MB/s | 0.3712920 | 18885.60 MB/s | | 32768 | 0.2852244 | 14507.93 MB/s | 0.3712709 | 18884.86 MB/s | | 65536 | 0.2851003 | 14501.41 MB/s | 0.3701114 | 18826.14 MB/s | | 131072 | 0.2850711 | 14499.95 MB/s | 0.3685017 | 18743.58 MB/s | | 262144 | 0.2850745 | 14500.47 MB/s | 0.3684799 | 18742.78 MB/s | | 524288 | 0.2848062 | 14486.66 MB/s | 0.3681040 | 18723.63 MB/s | | 1048576 | 0.2846679 | 14479.64 MB/s | 0.3671284 | 18674.02 MB/s | | 2097152 | 0.2840133 | 14446.52 MB/s | 0.3664623 | 18640.01 MB/s | | 4194304 | 0.2745241 | 13963.13 MB/s | 0.3488823 | 17745.24 MB/s | | 8388608 | 0.2741900 | 13946.39 MB/s | 0.3476036 | 17680.37 MB/s | | 16777216 | 0.2740689 | 13940.32 MB/s | 0.3477076 | 17685.97 MB/s | | 33554432 | 0.2746752 | 13970.75 MB/s | 0.3482017 | 17711.36 MB/s | --------------------------------------------------------------------------------
Seria ainda melhor ver como isso se comportaria em máquinas ainda mais antigas, diz Nehalem (supondo que ele tenha suporte a RCP
).
Implementação SSE41
+ FMA
Comparamos a divisão simples com o algoritmo proposto acima, permitindo FMA
e SSE41
=============================================================== = Compiler & System info =============================================================== Current CPU : Intel(R) Xeon(R) CPU E3-1285L v3 @ 3.10GHz CXX Compiler ID : GNU CXX Compiler Path : /usr/bin/c++ CXX Compiler Version : 4.9.2 CXX Compiler Flags : -O3 -std=c++11 -msse4.1 -mfma -------------------------------------------------------------------------------- | Size | Division F/C | Division B/W | Approx. F/C | Approximation B/W | -------------------------------------------------------------------------------- | 128 | 0.5714286 | 26884.20 MB/s | 0.5423729 | 25506.41 MB/s | | 256 | 0.5701559 | 26879.92 MB/s | 0.5412262 | 25503.95 MB/s | | 512 | 0.5701559 | 26904.68 MB/s | 0.5423729 | 25584.65 MB/s | | 1024 | 0.5704735 | 26911.46 MB/s | 0.5429480 | 25622.57 MB/s | | 2048 | 0.5704735 | 26915.03 MB/s | 0.5433802 | 25640.09 MB/s | | 4096 | 0.5703941 | 26917.72 MB/s | 0.5435965 | 25651.63 MB/s | | 8192 | 0.5703544 | 26915.85 MB/s | 0.5436687 | 25656.76 MB/s | | 16384 | 0.5699972 | 26898.44 MB/s | 0.5262583 | 24834.54 MB/s | | 32768 | 0.5699873 | 26898.93 MB/s | 0.5262076 | 24833.21 MB/s | | 65536 | 0.5698882 | 26894.48 MB/s | 0.5250567 | 24778.35 MB/s | | 131072 | 0.5697024 | 26885.50 MB/s | 0.5224302 | 24654.59 MB/s | | 262144 | 0.5696950 | 26884.72 MB/s | 0.5223095 | 24649.49 MB/s | | 524288 | 0.5696937 | 26885.37 MB/s | 0.5223308 | 24650.21 MB/s | | 1048576 | 0.5690340 | 26854.14 MB/s | 0.5220133 | 24634.71 MB/s | | 2097152 | 0.5455717 | 25746.56 MB/s | 0.5041949 | 23794.65 MB/s | | 4194304 | 0.5125461 | 24188.11 MB/s | 0.4756604 | 22447.05 MB/s | | 8388608 | 0.5043430 | 23800.67 MB/s | 0.4659974 | 21991.51 MB/s | | 16777216 | 0.5017375 | 23677.94 MB/s | 0.4614457 | 21776.58 MB/s | | 33554432 | 0.5005865 | 23623.50 MB/s | 0.4596277 | 21690.63 MB/s | --------------------------------------------------------------------------------
FMA
+ SSE4.1
nos dá algum nível de melhorias, mas isso não é bom o suficiente.
Implementação AVX2
+ FMA
Finalmente, podemos ver um benefício real comparando a divisão simples do AVX2
com o método de aproximação:
=============================================================== = Compiler & System info =============================================================== Current CPU : Intel(R) Xeon(R) CPU E3-1285L v3 @ 3.10GHz CXX Compiler ID : GNU CXX Compiler Path : /usr/bin/c++ CXX Compiler Version : 4.9.2 CXX Compiler Flags : -O3 -std=c++11 -march=haswell -------------------------------------------------------------------------------- | Size | Division F/C | Division B/W | Approx. F/C | Approximation B/W | -------------------------------------------------------------------------------- | 128 | 0.5663717 | 26672.73 MB/s | 0.9481481 | 44627.89 MB/s | | 256 | 0.5651214 | 26653.72 MB/s | 0.9481481 | 44651.56 MB/s | | 512 | 0.5644983 | 26640.36 MB/s | 0.9463956 | 44660.99 MB/s | | 1024 | 0.5657459 | 26689.41 MB/s | 0.9552239 | 45044.21 MB/s | | 2048 | 0.5662151 | 26715.40 MB/s | 0.9624060 | 45405.33 MB/s | | 4096 | 0.5663717 | 26726.27 MB/s | 0.9671783 | 45633.64 MB/s | | 8192 | 0.5664500 | 26732.42 MB/s | 0.9688941 | 45724.83 MB/s | | 16384 | 0.5699377 | 26896.04 MB/s | 0.9092624 | 42909.11 MB/s | | 32768 | 0.5699675 | 26897.85 MB/s | 0.9087077 | 42883.21 MB/s | | 65536 | 0.5699625 | 26898.59 MB/s | 0.9001456 | 42480.91 MB/s | | 131072 | 0.5699253 | 26896.38 MB/s | 0.8926057 | 42124.09 MB/s | | 262144 | 0.5699117 | 26895.58 MB/s | 0.8928610 | 42137.13 MB/s | | 524288 | 0.5698622 | 26892.87 MB/s | 0.8928002 | 42133.63 MB/s | | 1048576 | 0.5685829 | 26833.13 MB/s | 0.8894302 | 41974.25 MB/s | | 2097152 | 0.5558453 | 26231.90 MB/s | 0.8371921 | 39508.55 MB/s | | 4194304 | 0.5224387 | 24654.67 MB/s | 0.7436747 | 35094.81 MB/s | | 8388608 | 0.5143588 | 24273.46 MB/s | 0.7185252 | 33909.08 MB/s | | 16777216 | 0.5107452 | 24103.19 MB/s | 0.7133449 | 33664.28 MB/s | | 33554432 | 0.5101245 | 24074.10 MB/s | 0.7125114 | 33625.03 MB/s | --------------------------------------------------------------------------------
Este método pode definitivamente fornecer uma aceleração contra a divisão simples. Quanta aceleração pode realmente ser obtida, isso realmente depende da arquitetura subjacente, bem como de como a divisão interage com o restante da lógica do aplicativo.