Notei uma coisa curiosa no meu computador. * O teste de divisibilidade manuscrita é significativamente mais rápido que o %
operador. Considere o exemplo mínimo:
* AMD Ryzen Threadripper 2990WX, GCC 9.2.0
static int divisible_ui_p(unsigned int m, unsigned int a)
{
if (m <= a) {
if (m == a) {
return 1;
}
return 0;
}
m += a;
m >>= __builtin_ctz(m);
return divisible_ui_p(m, a);
}
O exemplo é limitado por ímpar a
e m > 0
. No entanto, pode ser facilmente generalizado para todos a
e m
. O código apenas converte a divisão em uma série de adições.
Agora considere o programa de teste compilado com -std=c99 -march=native -O3
:
for (unsigned int a = 1; a < 100000; a += 2) {
for (unsigned int m = 1; m < 100000; m += 1) {
#if 1
volatile int r = divisible_ui_p(m, a);
#else
volatile int r = (m % a == 0);
#endif
}
}
... e os resultados no meu computador:
| implementation | time [secs] |
|--------------------|-------------|
| divisible_ui_p | 8.52user |
| builtin % operator | 17.61user |
Portanto, mais de 2 vezes mais rápido.
A pergunta: você pode me dizer como o código se comporta na sua máquina? Perdeu a oportunidade de otimização no GCC? Você pode fazer esse teste ainda mais rápido?
UPDATE: Conforme solicitado, aqui está um exemplo reproduzível mínimo:
#include <assert.h>
static int divisible_ui_p(unsigned int m, unsigned int a)
{
if (m <= a) {
if (m == a) {
return 1;
}
return 0;
}
m += a;
m >>= __builtin_ctz(m);
return divisible_ui_p(m, a);
}
int main()
{
for (unsigned int a = 1; a < 100000; a += 2) {
for (unsigned int m = 1; m < 100000; m += 1) {
assert(divisible_ui_p(m, a) == (m % a == 0));
#if 1
volatile int r = divisible_ui_p(m, a);
#else
volatile int r = (m % a == 0);
#endif
}
}
return 0;
}
compilado com o gcc -std=c99 -march=native -O3 -DNDEBUG
AMD Ryzen Threadripper 2990WX com
gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0
UPDATE2: Conforme solicitado, a versão que pode lidar com qualquer um ( a
e m
se você também deseja evitar o excesso de números inteiros, o teste deve ser implementado com o tipo inteiro duas vezes mais do que os números inteiros de entrada):
int divisible_ui_p(unsigned int m, unsigned int a)
{
#if 1
/* handles even a */
int alpha = __builtin_ctz(a);
if (alpha) {
if (__builtin_ctz(m) < alpha) {
return 0;
}
a >>= alpha;
}
#endif
while (m > a) {
m += a;
m >>= __builtin_ctz(m);
}
if (m == a) {
return 1;
}
#if 1
/* ensures that 0 is divisible by anything */
if (m == 0) {
return 1;
}
#endif
return 0;
}
r
s que você calcula são realmente iguais um ao outro.a % b
temb
muito menor quea
. Na maioria das iterações do seu caso de teste, elas têm tamanho semelhante oub
é maior e sua versão pode ser mais rápida em muitas CPUs nessas situações.Respostas:
O que você está fazendo é chamado de redução de força: substituindo uma operação cara por uma série de operações baratas.
A instrução mod em muitas CPUs é lenta, porque historicamente não foi testada em vários benchmarks comuns e, portanto, os designers otimizaram outras instruções. Esse algoritmo terá um desempenho pior se for necessário fazer muitas iterações e
%
terá um desempenho melhor em uma CPU em que precisará apenas de dois ciclos de clock.Por fim, esteja ciente de que existem muitos atalhos para obter o restante da divisão por constantes específicas. (Embora os compiladores geralmente cuidem disso para você.)
fonte
div
/idiv
que adquiriu algum amor na Intel Penryn, Broadwell e IceLake (divisores de hardware com maior radix)x = i * const
cada iteração, você fazx += const
todas as iterações. Eu não acho que substituir uma única multiplicação por um loop de deslocamento / adição seria chamado de redução de força. en.wikipedia.org/wiki/… diz que o termo talvez possa ser usado dessa maneira, mas com uma nota "Este material é contestado. É melhor descrito como otimização do olho mágico e atribuição de instruções".Eu mesmo vou responder minha pergunta. Parece que me tornei vítima da previsão de ramificações. O tamanho mútuo dos operandos não parece importar, apenas a ordem deles.
Considere a seguinte implementação
e as matrizes
que são / não são embaralhadas usando o modo aleatório função .
Sem embaralhar, os resultados ainda são
No entanto, depois de embaralhar essas matrizes, os resultados são diferentes
fonte