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

  • Em PCs, isso geralmente é feito em hardware (o x86 e IDIV, que realizam a divisão inteira, também armazenam o restante em um registro). Você está perguntando como o hardware faz isso?
  • Leituras relacionadas: Linguagem Assembly – Como fazer Módulo?
  • A divisão longa funciona da mesma forma com números binários e decimais. Experimente, depois pense em como codificá-lo (dica: você ‘ usará muitos turnos).
  • @DJMcMayhem: Sim. Às vezes, o compilador precisa realizar alguns ajustes nos resultados para ficar em conformidade com a especificação da linguagem para aritmética assinada. Em geral, a velocidade da divisão do hardware é boa o suficiente para que, quando você não souber os valores de entrada no momento da compilação, seja melhor deixar o hardware fazer isso em vez da emulação do software. No entanto, se o divisor for conhecido em tempo de compilação, existem truques que podem ser executados mais rapidamente. Veja: isto e isto .
  • Observe que a maneira como você estão cronometrando coisas; na verdade, você está apenas medindo o tempo que leva para cout << a maioria das operações. Duvido que você pudesse realmente medir o tempo que o processador leva para fazer 20 % 3 mesmo se o compilador não ‘ o fizesse em tempo de compilação de qualquer maneira .

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.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *