Não estou perguntando como funciona o conceito matemático de módulo ou como 20 % 3 == 2
entendo isso. Estou mais curioso para saber como o compilador / intérprete determina isso. Posso pensar em uma maneira muito ingênua de fazer isso, como:
int modulus(int a, int b) { while (a > b) { a -= b; } return a; }
(obviamente, são necessários mais alguns casos porque a pode ser menor que b, mas você entendeu.) Mas isso teria alguns problemas sérios de desempenho. Eu juntei um pouco de C ++ para testar a eficiência de ambos os módulos operador e minha função ingênua.
#include <iostream> #include <chrono> #include <limits> #include "chrono_io" using namespace std; typedef chrono::high_resolution_clock Clock; int mod(int a, int b) { while (a > b) { a -= b; } return a; } int main() { auto start = Clock::now(); cout << 20 % 3 << endl; auto end = Clock::now(); cout << "Modulus on a small number took " << end - start << " seconds to calculate.\n"; start = Clock::now(); cout << numeric_limits<int>::max() % 3 << endl; end = Clock::now(); cout << "Modulus on a large number took " << end - start << " seconds to calculate.\n"; start = Clock::now(); cout << mod(20, 3) << endl; end = Clock::now(); cout << "My modulus function on a small number took " << end - start << " seconds to calculate.\n"; start = Clock::now(); cout << mod(numeric_limits<int>::max(), 3) << endl; end = Clock::now(); cout << "My modulus function on a large number took " << end - start << " seconds to calculate.\n"; }
e a saída é:
2 Modulus on a small number took 43629 nanoseconds seconds to calculate. 1 Modulus on a large number took 5644 nanoseconds seconds to calculate. 2 My modulus function on a small number took 3682 nanoseconds seconds to calculate. 1 My modulus function on a large number took 2153824554 nanoseconds seconds to calculate.
O O operador de módulo variou de 4.000 a 70.000 nanossegundos. Minha função sempre venceu nos números pequenos, mas levou cerca de 2 segundos no número grande. Como o operador de módulo é muito mais consistente, presumo que ele compare o número em um nível bit a bit.
Então, como funciona o operador de módulo? Posso pensar em alguns casos que seriam muito fáceis de determinar. Por exemplo, com o mod 2, você pode apenas olhar para o último byte. Ou com o mod 4 você pode olhar para o penúltimo b yte. Mas nem todos os números têm um padrão tão simples.
Comentários
Resposta
Quase todas as CPUs têm uma única instrução que retornará o módulo de um valor. Por exemplo, considere este programa:
int main() { int i = 10; return i % 3; }
Se eu compilar isso em minha máquina Intel OS X usando g ++ -S, o resultado será um boilerplate e este:
movl $3, %eax movl $0, -4(%rbp) movl $10, -8(%rbp) movl -8(%rbp), %ecx movl %eax, -12(%rbp) ## 4-byte Spill movl %ecx, %eax cltd movl -12(%rbp), %ecx ## 4-byte Reload idivl %ecx movl %edx, %eax
O módulo real acontece com esta instrução: idivl %ecx
. Quando esta instrução for executada, o quociente será colocado em %eax
e o restante será colocado em %edx
.
Visto que, na verdade, isso significa que a operação %
levará apenas alguns ciclos de clock, a maior parte deles movendo os dados para o registrador correto. Observe também que pelo menos com a Intel, a mesma operação encontra o quociente e o restante, portanto, em referência ao seu comentário, /
e %
leva exatamente o mesmo tempo. É a mesma operação. A única coisa que muda é de qual registro o compilador obtém os resultados.
Com qualquer CPU feita nas últimas duas décadas, você pode assumir que qualquer operação matemática básica (incluindo coisas que se parecem com funções de biblioteca como sqrt
ou cos
) é na verdade uma única instrução de máquina e geralmente leva apenas alguns ciclos de clock no máximo.
[ATUALIZAÇÃO]
Como as pessoas notaram nos comentários, para ver algo se aproximando do momento correto, você precisa remover a saída da seção cronometrada como esta:
int i; auto start = Clock::now(); i = 20 % 3; auto end = Clock::now(); cout << i << endl;
Mas mesmo isso provavelmente não é preciso, pois a granularidade real dos tempos pode exceder o que você é tentando tempo. Em vez disso, você pode querer fazer isso:
int i=0; int x; auto start = Clock::now(); for(i=0;i<1000000;i++) x = i % 3; auto end = Clock::now(); cout << i << endl;
Em seguida, divida seu tempo por 1.000.000. (Isso será um pouco alto, pois inclui o tempo gasto para uma tarefa, uma comparação e um incremento.) Na minha máquina, isso dá um tempo de 5 nanossegundos.
Comentários
- Sério? Disseram-me que sqrt é uma instrução muito lenta.
- Consulte stackoverflow.com / questions / 7724061 / …
- @StevenBurnap essa resposta está um pouco desatualizada, com base na arquitetura do ano de 2011. De acordo com Agner Fog ‘ tabela s, que é atualizada pelo menos anualmente , instruções SSE2 de Haswell SQRTSS / SQRTPS (float de 32 bits, com 23 bits de significância) tem uma latência de 11 ciclos, SQRTSD / SQRTPD (flutuante de 64 bits) tem uma latência de 16 ciclos. Estas instruções são precisas, ou seja, não das variantes de aproximação rápida. Na verdade, é muito mais lento em arquiteturas mais antigas. Além disso, lembre-se de que chamar
std::sqrt()
incorre em uma chamada de biblioteca na maioria dos compiladores C ++. - Os compiladores também podem aplicar otimização de redução de força para casos especiais. Por exemplo, x% 2 pode ser otimizado para uma máscara de bits se ‘ for mais rápido em uma arquitetura específica.
- @rwong, pensei que
std:sqrt
normalmente seria embutido (e usaria montagem embutida). Mas eu ‘ nunca verifiquei.
IDIV
, que realizam a divisão inteira, também armazenam o restante em um registro). Você está perguntando como o hardware faz isso?cout <<
a maioria das operações. Duvido que você pudesse realmente medir o tempo que o processador leva para fazer20 % 3
mesmo se o compilador não ‘ o fizesse em tempo de compilação de qualquer maneira .