À medida que me envolvo cada vez mais com a teoria por trás da programação, sinto-me fascinado e aturdido por coisas aparentemente simples. Percebo que minha compreensão da maioria dos processos fundamentais é justificada através da lógica circular
P : Como isso funciona?
A : Porque sim!
Eu odeio essa realização! Adoro o conhecimento e, além disso, adoro aprender, o que me leva à minha pergunta (embora seja ampla).
Questão:
Como os operadores matemáticos fundamentais são avaliados com linguagens de programação?
Como os métodos atuais foram aprimorados?
Exemplo
var = 5 * 5;
Minha interpretação:
$num1 = 5; $num2 = 5; $num3 = 0;
while ($num2 > 0) {
$num3 = $num3 + $num1;
$num2 = $num2 - 1;
}
echo $num3;
Isso parece ser altamente ineficiente. Com fatores mais altos, esse método é muito lento, enquanto o método incorporado padrão é instantâneo. Como você simularia a multiplicação sem adicionar iterações?
var = 5 / 5;
Como isso é feito? Não consigo pensar em uma maneira de literalmente dividir 5 em 5 partes iguais.
var = 5 ^ 5;
Iterações de iterações de adição? Minha interpretação:
$base = 5;
$mod = 5;
$num1 = $base;
while ($mod > 1) {
$num2 = 5; $num3 = 0;
while ($num2 > 0) {
$num3 = $num3 + $num1;
$num2 = $num2 - 1;
}
$num1 = $num3;
$mod -=1;
}
echo $num3;
Novamente, isso é EXTREMAMENTE ineficiente, mas não consigo pensar em outra maneira de fazer isso. Essa mesma pergunta se estende a todas as funções matemáticas relacionadas que são tratadas automagicamente.
fonte
Respostas:
Para realmente entender como a aritmética funciona dentro de um computador, você precisa ter programado em linguagem assembly. De preferência um com tamanho de palavra pequeno e sem instruções de multiplicação e divisão. Algo como o 6502.
No 6502, praticamente toda aritmética é feita em um registro chamado Accumulator. (Um registro é um local de memória especial dentro do processador que pode ser acessado rapidamente.) Portanto, para adicionar dois números, você carrega o primeiro número no Acumulador e depois o segundo número.
Mas isso é simplista demais. Como o 6502 é um processador de 8 bits, ele pode manipular números apenas de 0 a 255. Na maioria das vezes, você desejará poder trabalhar com números maiores. Você deve adicioná-los em pedaços, 8 bits por vez. O processador possui um sinalizador Carry que é definido quando o resultado da adição de dois números excede o acumulador. O processador adiciona isso ao fazer uma adição, para que possa ser usado para "carregar o 1", supondo que você comece com o byte de ordem mais baixa de um número. Uma adição de vários bytes no 6502 se parece com isso:
A subtração é semelhante, exceto se você definir o transporte primeiro, use a instrução SBC em vez do ADC e, no final, o transporte ficará limpo se houver fluxo insuficiente.
Mas espere! E os números negativos? Bem, com o 6502, eles são armazenados em um formato chamado complemento de dois. Supondo que um número de 8 bits, -1 seja armazenado como 255, porque quando você adiciona 255 a algo, você recebe um a menos no Acumulador (mais um carry). -2 é armazenado como 254 e assim por diante, até -128, que é armazenado como 128. Portanto, para números inteiros assinados, metade do intervalo de 0 a 255 de um byte é usada para números positivos e metade para números negativos. (Essa convenção permite verificar o número alto de um número para ver se é negativo.)
Pense nisso como um relógio de 24 horas: adicionar 23 à hora resultará em uma hora uma hora antes (no dia seguinte). Então 23 é o equivalente modular do relógio a -1.
Quando você estiver usando mais de 1 byte, precisará usar números maiores para negativos. Por exemplo, números inteiros de 16 bits têm um intervalo de 0 a 65536. Portanto, 65535 é usado para representar -1, e assim por diante, porque adicionar 65535 a qualquer número resulta em um a menos (mais um carry).
No 6502, existem apenas quatro operações aritméticas: somar, subtrair, multiplicar por dois (deslocar para a esquerda) e dividir por dois (deslocar para a direita). A multiplicação e a divisão podem ser feitas usando apenas essas operações ao lidar em binário. Por exemplo, considere multiplicar 5 (binário 101) e 3 (binário 11). Como na multiplicação decimal longa, começamos com o dígito direito do multiplicador e multiplicamos 101 por 1, resultando em 101. Em seguida, deslocamos o multiplicando para a esquerda e multiplicamos 1010 por 1, resultando em 1010. Em seguida, adicionamos esses resultados, fornecendo 1111 ou 15. Como sempre multiplicamos por 1 ou 0, na verdade não multiplicamos; cada bit do multiplicador simplesmente serve como uma bandeira que indica se devemos adicionar ou não o multiplicando (deslocado).
Divisão é análoga à divisão longa manual usando divisores de teste, exceto em binário. Se você estiver dividindo por uma constante, é possível fazer isso de maneira análoga à subtração: em vez de dividir por X, você multiplica por uma representação pré-calculada de 1 / X que produz o resultado desejado mais um estouro. Ainda hoje, isso é mais rápido que a divisão.
Agora tente fazer matemática de ponto flutuante na montagem ou converter números de ponto flutuante em bons formatos de saída na montagem. E lembre-se, é 1979 e a velocidade do relógio é de 1 MHz, então você deve fazê-lo da maneira mais eficiente possível.
As coisas ainda funcionam praticamente assim hoje, exceto com tamanhos maiores de palavras e mais registros, e é claro que a maior parte da matemática é feita por hardware agora. Mas ainda é feito da mesma maneira fundamental. Se você adicionar o número de turnos e os acréscimos necessários para uma multiplicação, por exemplo, ele se correlacionará bastante bem com o número de ciclos necessários para uma instrução de multiplicação de hardware nos primeiros processadores com uma instrução como a 6809, onde foi executada no microcódigo da mesma maneira que você faria manualmente. (Se você tiver um orçamento maior de transistor, existem maneiras mais rápidas de fazer as trocas e as adições, para que os processadores modernos não executem essas operações sequencialmente e possam realizar multiplicações em apenas um ciclo.)
fonte
Binary Multiplier
para obter detalhes.No final, operações aritméticas básicas são feitas em hardware. Mais especificamente, na CPU (ou na verdade, uma subparte dela)
Em outras palavras, são circuitos eletrônicos. Defina os bits apropriados como entrada e você obterá os bits apropriados como saída. É uma combinação de portas lógicas básicas.
http://en.wikipedia.org/wiki/Adder_%28electronics%29
http://en.wikipedia.org/wiki/Binary_multiplier
fonte
Tudo isso é coberto com paciência em The Art of Computer Programming, de Don Knuth.
Algoritmos eficientes para adicionar, subtrair, multiplicar e dividir são todos descritos em detalhes completos.
Você pode ler coisas como esta que cobrem bem a divisão.
http://research.microsoft.com/pubs/151917/divmodnote.pdf
fonte
É feito em picossegundos por circuitos eletrônicos. Google 'multiplicador de hardware' etc. para obter detalhes. As CPUs modernas são o resultado extremamente complexo de décadas de melhoria contínua.
BTW, Como você não se multiplica por adição repetida, por que você imaginaria que um computador faria?
fonte
Isso não pretende ser uma resposta completa de forma alguma, mas deve lhe dar uma idéia de como as coisas são implementadas. Como você provavelmente sabe, os números são representados em binário. Por exemplo, um computador pode representar o número 5 como 00000101. Uma operação muito básica que um computador pode fazer é deslocar para a esquerda, o que daria 00001010 que é decimal 10. Se fosse o deslocador direito duas vezes, seria 00010100 (decimal 20). Toda vez que mudamos os dígitos para a esquerda 1 vez dobramos o número. Digamos que eu tenha um número x e que eu queira multiplicá-lo por 17. Eu poderia mudar x para a esquerda 4 vezes e depois adicionar x ao resultado (16x + x = 17x). Essa seria uma maneira eficiente de multiplicar um número por 17. Isso deve lhe dar algumas dicas sobre como um computador pode multiplicar números grandes sem usar apenas a adição repetida.
A divisão pode usar combinações de adição, subtração, deslocamento para a direita, deslocamento para a esquerda, etc. Existem também vários truques para aumentar o número de expoentes.
fonte
shl r0, 4
.Quando eu era criança, aprendi a multiplicar e dividir com uma caneta e um papel, sem perder tempo com muitas adições. Mais tarde, aprendi que as raízes quadradas também são computáveis dessa maneira.
Na universidade, aprendi a calcular operações trigonométricas e logarítmicas com uma dúzia de multiplicações, divisões e adições. Eles chamaram de série Taylor.
Antes disso, meu pai me deu um livro onde essas operações complexas já eram computadas para centenas de valores e apresentadas em tabelas. Havia também algumas explicações para estimar o erro quando você desejava o seno de um valor entre dois valores calculados.
Unidades inteiras, unidades de ponto flutuante, GPU e DSP apenas implementam todas essas técnicas antigas em silício.
fonte
Tentarei dar uma idéia de como os circuitos digitais são projetados para resolver problemas de processamento digital usando os problemas que você apresenta: como as CPUs implementam adições e multiplicações.
Primeiro, vamos tirar a questão direta do caminho: como uma linguagem de programação avalia eficientemente multiplicações e acréscimos. A resposta é simples, eles os compilam para multiplicar e adicionar instruções. Por exemplo, o seguinte código:
é simplesmente compilado para algo como:
(observe que o assembly acima é para uma CPU imaginária que não existe, por uma questão de simplicidade).
Nesse ponto, você percebe que a resposta acima simplesmente muda o problema e o resolve pela mágica do hardware. A pergunta seguinte é obviamente como essa mágica de hardware funciona?
Vamos analisar primeiro o problema mais simples: adição.
Primeiro, fazemos um problema familiar, adicionando regularmente números de base 10:
O primeiro passo seria adicionar 7 e 8. Mas isso resulta em 15, que é mais do que um único dígito. Então, carregamos o 1:
Agora adicionamos 1, 1 e 2 juntos:
Então, a partir disso, obtemos as seguintes regras:
quando o resultado da adição é superior a um dígito, mantemos o dígito menos significativo e avançamos com o dígito mais significativo
se temos um dígito transportado para a nossa coluna, o adicionamos juntamente com os números que estamos adicionando
Agora é hora de interpretar as regras acima na base 2 - álgebra booleana.
Assim, na álgebra booleana, adicionando 0 e 1 juntos = 1. Adicionando 0 e 0 = 0. E adicionando 1 e 1 = 10, que é mais de um dígito, para que continuemos com o 1.
A partir disso, podemos construir uma tabela de verdade:
A partir disso, podemos construir dois circuitos / equações booleanas - uma para a saída da soma e outra para a saída do carry. A maneira mais ingênua é simplesmente listar todas as entradas. Qualquer tabela verdade, não importa quão grande e complexa possa ser reapresentada desta forma:
Esta é basicamente a soma da forma dos produtos. Nós olhamos apenas as saídas que resultam em 1 e ignoramos os 0s:
Vamos substituir AND e NOT por símbolos da linguagem de programação para facilitar a leitura:
Basicamente, convertemos a tabela da seguinte maneira:
Isso pode ser implementado diretamente como um circuito:
Os leitores observadores nesse momento perceberiam que a lógica acima pode realmente ser implementada como uma única porta - uma porta XOR que possui convenientemente o comportamento exigido por nossa tabela de verdade:
Mas se o seu hardware não fornecer um portão XOR, as etapas acima são como definir e implementar em termos de portas AND, OR e NOT.
Como você converteria portas lógicas em hardware real depende do hardware que você possui. Eles podem ser implementados usando vários mecanismos físicos, desde que o mecanismo forneça algum tipo de comportamento de comutação. Os portões lógicos foram implementados com tudo, desde jatos de água ou sopros de ar (fluídicos) a transisitores (eletrônicos) a bolas de gude caindo. É um grande tópico por si só, então vou descrevê-lo e dizer que é possível implementar portas lógicas como dispositivos físicos.
Agora fazemos o mesmo para o sinal de transporte. Como existe apenas uma condição em que o sinal de transporte é verdadeiro, a equação é simplesmente:
Portanto, carregar é simples:
Combinando-os, obtemos o que é conhecido como meio adicionador:
As equações para o circuito acima, a propósito, são assim:
O meio adicionador está faltando alguma coisa. Implementamos a primeira regra - se o resultado for mais de um dígito do que transportar, mas não implementamos a segunda regra - se houver um transportador, adicione-o juntamente com os números.
Portanto, para implementar um somador completo, um circuito de adição que pode adicionar números com mais de um dígito, precisamos definir uma tabela verdade:
A equação da soma é agora:
Podemos passar pelo mesmo processo de fatorar e simplificar a equação e interpretá-la como um circuito etc., como fizemos acima, mas acho que essa resposta está ficando muito longa.
Agora você deve ter uma idéia de como a lógica digital é projetada. Existem outros truques que não mencionei, como mapas de Karnaugh (usados para simplificar tabelas verdadeiras) e compiladores lógicos, como café expresso (para que você não precise fatorar equações booleanas à mão), mas o básico é basicamente o que eu tenho. descrito acima:
Decomponha o problema até poder trabalhar no nível de bit único (dígito).
Defina as saídas que você deseja usando uma tabela verdade.
Converta a tabela em uma equação booleana e simplifique a equação.
Interprete a equação como portas lógicas.
Converta seu circuito lógico em circuitos reais de hardware implementando portas lógicas.
É assim que os problemas fundamentais (ou melhor, de baixo nível) são realmente resolvidos - muitas e muitas tabelas verdadeiras. O verdadeiro trabalho criativo está no detalhamento de uma tarefa complexa, como decodificação de MP3 no nível de bits, para que você possa trabalhar com tabelas verdadeiras.
Desculpe, não tenho tempo para explicar como implementar a multiplicação. Você pode tentar entender um pouco sobre isso, descobrindo regras de quanto tempo a multiplicação funciona e depois interpretando-a em binário, e tente dividi-la em tabelas verdadeiras. Ou você pode ler a Wikipedia: http://en.wikipedia.org/wiki/Binary_multiplier
fonte
instruções aritméticas básicas são executadas com instruções de montagem que são altamente eficientes.
Instruções mais complexas (ou abstratas) são feitas em montagem com mecanismos de loop ou são tratadas em bibliotecas std.
Ao estudar matemática na faculdade, você começará a estudar coisas como o cálculo Lambda e a notação Big-O. Tudo isso, e muito mais, é usado pelos programadores para avaliar e criar algoritmos eficientes. De qualquer forma, o material básico geralmente é realizado em nível inferior, como na montagem ou em c com ponteiros.
Uma ótima introdução a esse tópico é "Code", de Charles Petzold.
fonte
Obtenha um livro como Fundamentals of Digital Logic ... , que eu acho que ainda é bastante padrão para os alunos do Freshman / Sophomore EE, e trabalhe nele (ed: custa uma pequena fortuna, procure uma edição usada ou anterior de isto). Isso o guiará pelos somadores e multiplicadores e fornecerá um histórico suficiente para começar a entender alguns dos princípios por trás do que o hardware está fazendo.
Sua resposta, no curto prazo, se tornará "porque combina várias peças de lógica mais simples para evocar esse comportamento complexo" em vez de "porque o faz".
Se você quiser tentar entender todos os princípios de como os programas são compilados e executados, faça isso em conjunto, para que você possa ver como tudo se encontra no meio.
fonte
Há muitas boas respostas aqui. Você também começou com a idéia certa: operações complexas como multiplicação são criadas a partir de operações mais simples. Como você adivinhou, existem maneiras mais rápidas de multiplicar sem uma instrução de multiplicação do que usar uma série de adições. Qualquer multiplicação pode ser implementada como a soma de multiplicações menores ou como uma combinação de turnos e adições. Exemplos:
A divisão também pode ser dividida em operações menores. XOR (^) é uma instrução embutida em todos os processadores que eu já vi, mas mesmo assim ele pode ser implementado como uma combinação de AND, OR e NOT.
No entanto, sinto que respostas específicas serão menos satisfatórias para você do que uma idéia geral dos tipos de instruções que um processador fornece e como essas instruções podem ser combinadas em operações mais complexas. Não há nada melhor para esse tipo de curiosidade do que uma dose saudável de linguagem assembly. Aqui está uma introdução muito acessível à linguagem assembly MIPS.
fonte
Veja como um processador moderno pode implementar a multiplicação de dois números inteiros de 64 bits:
Você sabe como fazer uma multiplicação de mão longa. Para multiplicar dois números de 10 dígitos, multiplique um número de 10 dígitos por cada um dos 10 dígitos do outro número, escreva o resultado de 11 dígitos um abaixo do outro e desloque-o e depois adicione todo o número.
Um processador moderno faz isso com todos os 64 por 64 bits. No entanto, uma multiplicação de dois números de bit único é muito simples: 1 x 1 = 1, todos os outros produtos são zero. Isso é implementado com uma lógica e. E, diferentemente do produto decimal, em que o resultado pode ter dois dígitos, um produto binário de números de bits únicos é sempre um bit.
Então agora você tem 64 linhas de 64 bits que precisam ser adicionadas. Mas 64 adições de números de 64 bits são slooooooow. Portanto, o processador usa um somador de 3/2 ou um somador de 7/3: se você adicionar três números de bit único, o resultado poderá ser 0, 1, 2 ou 3, o que se encaixa em dois bits. Se você adicionar 7 números de bit único, o resultado será um número de 0 a 7, que pode ser representado por 3 bits. A IBM afirma que eles podem criar um somador 7/3 com apenas 18 circuitos primitivos (documentação do PowerPC), aposto que a Intel e o ARM também podem fazer isso.
Você tem 4096 bits, agrupe-os em cerca de 600 grupos de 7 bits nas mesmas posições de bits e use cerca de 600 somadores 7/3 para reduzir o resultado de 4096 bits para menos de 2.000. Então você faz o mesmo novamente, e novamente, até acabar com pares de bits que podem ser alimentados em um somador completo comum.
fonte