Dado um sistema de computador específico, é possível estimar o tempo de execução exato e real de uma parte do código de montagem

23

este é um pedaço de código de montagem

section .text
    global _start       ;must be declared for using gcc
_start:                     ;tell linker entry point
    mov edx, len    ;message length
    mov ecx, msg    ;message to write
    mov ebx, 1      ;file descriptor (stdout)
    mov eax, 4      ;system call number (sys_write)
    int 0x80        ;call kernel
    mov eax, 1      ;system call number (sys_exit)
    int 0x80        ;call kernel

section .data

msg db  'Hello, world!',0xa ;our dear string
len equ $ - msg         ;length of our dear string

Dado um sistema de computador específico, é possível prever com precisão o tempo real de execução de uma parte do código de montagem.

yaojp
fonte
30
"Executar o código nesse computador e usar um cronômetro" é uma resposta válida?
Draconis
4
Suspeito que a maior parte do tempo gasto na execução desse trecho de código esteja aguardando E / S. O tempo que leva para executar as instruções individuais é um tanto previsível se você soubesse o local da memória do código e todos os detalhes sobre o processador (hoje em dia extremamente complexos), mas a velocidade também é afetada pela memória e pelo disco. eu teria que conhecer uma quantidade muito grande de detalhes sobre eles também. Portanto, a menos que considere os fenômenos físicos (que também afetam o tempo), você poderia dizer que é previsível, mas é inimaginavelmente difícil fazê-lo.
IllidanS4 quer Monica de volta
4
é sempre possível estimar ...
sudo rm -rf slash
3
Isso também não é impossível devido ao problema de parada? Podemos provar para algum código se ele será interrompido, mas não podemos ter um algoritmo que determine isso para todos os códigos possíveis.
kutschkem 2/09
2
@ Falco Isso seria uma propriedade do sistema fornecido. Algumas implementações independentes de C não têm sistema operacional; tudo o que está sendo executado é um loop principal (ou nem mesmo um loop ;-)) que pode ou não ler os endereços de hardware para entrada.
Peter - Restabelece Monica

Respostas:

47

Só posso citar o manual de uma CPU bastante primitiva, um processador 68020, por volta de 1986: "É difícil calcular o tempo de execução exato de uma sequência de instruções, mesmo se você tiver conhecimento preciso da implementação do processador". O que não temos. E comparado a um processador moderno, essa CPU era primitiva .

Não posso prever o tempo de execução desse código, nem você. Mas você nem pode definir qual é o "tempo de execução" de um pedaço de código, quando um processador possui caches enormes e recursos massivos e fora de ordem. Um processador moderno típico pode ter 200 instruções "em voo", que estão em vários estágios de execução. Portanto, o tempo entre a tentativa de ler o primeiro byte da instrução e a retirada da última instrução pode ser bastante longo. Mas o atraso real em todo o restante trabalho que o processador precisa fazer pode ser (e normalmente é) muito menor.

É claro que fazer duas chamadas para o sistema operacional torna isso totalmente imprevisível. Você não sabe o que "escrever para stdout" realmente faz, portanto não pode prever o tempo.

E você não pode saber a velocidade do relógio do computador no momento exato em que executa o código. Pode estar em algum modo de economia de energia, o computador pode ter a velocidade do relógio reduzida porque ficou quente, portanto, mesmo o mesmo número de ciclos de relógio pode levar diferentes períodos de tempo.

Em suma: Totalmente imprevisível.

gnasher729
fonte
11
Eu acho que suas conclusões são muito fortes. Latência e taxa de transferência são métricas comuns para medir o "tempo de execução" de um programa. Além disso, você pode simplesmente optar por uma definição adequada de "tempo de execução". Além disso, se você tiver um instantâneo completo do estado do sistema, hw e sw, e um conhecimento perfeito dos internos da CPU, poderá prever o tempo de execução. Na Intel, eles provavelmente podem estimar o tempo de execução, mesmo aqui no SO podemos prever latências e tputs com precisão de ciclo. Nesse caso, além dos syscalls, não é tão difícil assim.
Margaret Bloom
9
@MargaretBloom nem então. Eu coloco meu telefone muito perto do forno, a CPU fica subclock para gerenciar a temperatura, sua estimativa de tempo de execução está subitamente muito baixa. E mesmo que você conte em ciclos e não faça syscalls, outros threads e CPUs podem funcionar muito bem com o conteúdo da RAM ou podem despejar sua memória no disco rígido enquanto você é trocado, com base em circunstâncias imprevisíveis, que variam de potência surtos diminuindo a velocidade do disco rígido apenas o suficiente para que um thread concorrente consiga memória suficiente a tempo de destruir o seu, todo o caminho até os threads rolando dados para ver quanto tempo desperdiçar.
John Dvorak
6
Além disso, "o conhecimento completo do estado do sistema, hw e sw" é uma tarefa bastante alta, parece. Adicione "10 ms de antecedência" e você já está pedindo o impossível. E se a implementação de geração aleatória de hardware do seu CPU usar fenômenos quânticos (provavelmente o faz), e algum segmento da CPU o chamar, então nem mesmo saber o estado completo do universo a 3000 km ao redor do computador o salvará. E no MWI, você nem consegue adivinhar direito.
John Dvorak
8
@ Nat: Mesmo em criptografia, "tempo constante" realmente não significa absolutamente constante - significa apenas que o tempo de execução não possui variações sistemáticas que dependem de dados secretos e podem ser estatisticamente correlacionadas com ele. E, na prática, é geralmente assumido que se o caminho do código adotado e o padrão de acesso à memória executado não dependem de dados secretos e se instruções específicas conhecidas por levar uma quantidade variável de tempo são evitadas (ou suas entradas são mascaradas) espero eliminar a correlação), provavelmente é bom o suficiente. Além disso, você realmente só precisa medir.
Ilmari Karonen 02/09
2
Um 68020 é um animal complexo ... tente um MCS51 ....
rackandboneman
29

Você não pode fazer isso em geral, mas, em alguns sentidos, pode muito bem, e houve alguns casos históricos em que você realmente precisou .

O Atari 2600 (ou Atari Video Computer System) foi um dos primeiros sistemas de videogame doméstico e foi lançado pela primeira vez em 1978. Ao contrário dos sistemas posteriores da época, a Atari não podia dar ao luxo de fornecer ao dispositivo um buffer de quadro, o que significa que a CPU tinha para executar o código em cada linha de verificação para determinar o que produzir - se esse código levasse 17,08 microssegundos para ser executado (o intervalo HBlank), os gráficos não seriam definidos corretamente antes que a linha de verificação começasse a desenhá-los. Pior, se o programador quisesse desenhar conteúdo mais complexo do que o Atari normalmente permitia, eles precisariam medir os horários exatos para obter instruções e alterar os registros gráficos à medida que o feixe estava sendo desenhado, com um intervalo de 57,29 microssegundos para toda a linha de varredura.

No entanto, o Atari 2600, como muitos outros sistemas baseados no 6502, tinha um recurso muito importante que possibilitou o gerenciamento cuidadoso do tempo necessário para este cenário: a CPU, a RAM e o sinal de TV funcionavam com o mesmo mestre. relógio. O sinal da TV disparou em um relógio de 3,98 MHz, dividindo os tempos acima em um número inteiro de "relógios coloridos" que gerenciavam o sinal da TV, e um ciclo dos relógios da CPU e da RAM eram exatamente três relógios coloridos, permitindo que o relógio da CPU fosse uma medida precisa do tempo em relação ao sinal atual da TV em andamento. (Para obter mais informações, consulte o Stella Programmer's Guide , escrito para o emulador Stella Atari 2600 ).

Além disso, esse ambiente operacional significava que todas as instruções de CPU tinham uma quantidade definida de ciclos necessários em todos os casos, e muitos 6502 desenvolvedores publicaram essas informações em tabelas de referência. Por exemplo, considere esta entrada para a CMPinstrução (Comparar memória com acumulador), obtida desta tabela :

CMP  Compare Memory with Accumulator

     A - M                            N Z C I D V
                                    + + + - - -

     addressing    assembler    opc  bytes  cycles
     --------------------------------------------
     immediate     CMP #oper     C9    2     2
     zeropage      CMP oper      C5    2     3
     zeropage,X    CMP oper,X    D5    2     4
     absolute      CMP oper      CD    3     4
     absolute,X    CMP oper,X    DD    3     4*
     absolute,Y    CMP oper,Y    D9    3     4*
     (indirect,X)  CMP (oper,X)  C1    2     6
     (indirect),Y  CMP (oper),Y  D1    2     5*

*  add 1 to cycles if page boundary is crossed

Usando todas essas informações, o Atari 2600 (e outros desenvolvedores do 6502) conseguiu determinar exatamente quanto tempo o código estava demorando para executar e construir rotinas que fizessem o que precisavam e que ainda cumprissem os requisitos de tempo de sinal de TV da Atari. E como esse tempo era tão exato (especialmente para instruções de perda de tempo como o NOP), eles foram capazes de usá-lo para modificar os gráficos à medida que eram desenhados.


Obviamente, o 6502 do Atari é um caso muito específico, e tudo isso é possível apenas porque o sistema tinha o seguinte:

  • Um relógio mestre que executava tudo, incluindo RAM. Os sistemas modernos têm relógios independentes para a CPU e a RAM, com o relógio da RAM geralmente sendo mais lento e os dois não necessariamente sincronizados.
  • Sem cache de qualquer tipo - o 6502 sempre acessava a DRAM diretamente. Os sistemas modernos têm caches SRAM que dificultam a previsão do estado - embora talvez ainda seja possível prever o comportamento de um sistema com um cache, é definitivamente mais difícil.
  • Nenhum outro programa foi executado simultaneamente - o programa no cartucho tinha controle total do sistema. Os sistemas modernos executam vários programas ao mesmo tempo usando algoritmos de determinação não determinísticos.
  • Uma velocidade de clock lenta o suficiente para que os sinais possam viajar através do sistema a tempo. Em um sistema moderno com velocidades de clock de 4 GHz (por exemplo), são necessários um fóton de luz de 6,67 ciclos de clock para percorrer o comprimento de uma placa-mãe de meio metro - você nunca pode esperar que um processador moderno interaja com outra coisa na placa em apenas um ciclo, já que são necessários mais de um ciclo para que um sinal na placa alcance o dispositivo.
  • Uma velocidade de clock bem definida que raramente muda (1,19 MHz no caso do Atari) - as velocidades da CPU dos sistemas modernos mudam o tempo todo, enquanto um Atari não poderia fazer isso sem afetar também o sinal da TV.
  • Cronogramas de ciclo publicados - o x86 não define quanto tempo suas instruções demoram.

Todas essas coisas se uniram para criar um sistema onde era possível criar conjuntos de instruções que levavam um tempo exato - e para esse aplicativo, era exatamente isso que era exigido. A maioria dos sistemas não possui esse grau de precisão simplesmente porque não há necessidade - os cálculos são feitos quando são feitos ou, se for necessária uma quantidade exata de tempo, um relógio independente pode ser consultado. Mas se a necessidade for adequada (como em alguns sistemas incorporados), ela ainda poderá aparecer e você poderá determinar com precisão quanto tempo seu código leva para ser executado nesses ambientes.


E também devo acrescentar o grande aviso de isenção de responsabilidade de que tudo isso se aplica apenas à construção de um conjunto de instruções de montagem que levarão uma quantidade exata de tempo. Se o que você quer fazer é pegar uma peça de montagem arbitrária, mesmo nesses ambientes, e perguntar "Quanto tempo isso leva para ser executado?", Você categoricamente não pode fazer isso - esse é o Problema da Parada , que foi provado insolúvel.


EDIT 1: Em uma versão anterior desta resposta, afirmei que o Atari 2600 não tinha como informar ao processador onde estava o sinal de TV, o que o forçou a manter o programa inteiro contado e sincronizado desde o início. Conforme apontado nos comentários, isso é válido para alguns sistemas como o ZX Spectrum, mas o Atari 2600, pois contém um registro de hardware que interrompe a CPU até que ocorra o próximo intervalo de apagamento horizontal, bem como uma função para iniciar o intervalo de apagamento vertical à vontade. Portanto, o problema da contagem de ciclos é limitado a cada linha de verificação e só se torna exato se o desenvolvedor desejar alterar o conteúdo à medida que a linha de verificação estiver sendo desenhada.

TheHansinator
fonte
3
Também deve ser observado que a maioria dos jogos não funcionou perfeitamente - você pode ver muitos artefatos na saída de vídeo devido ao tempo incompatível do sinal de vídeo, devido a erro do programador (estimativa incorreta do tempo da CPU) ou simplesmente tendo muito trabalho para fazer. Também era muito frágil - se você precisasse corrigir um bug ou adicionar novos recursos, provavelmente quebraria o tempo, às vezes inevitavelmente. Foi divertido, mas também um pesadelo :) Eu nem tenho certeza se a velocidade do relógio estava sempre exatamente correta - por exemplo, sob superaquecimento, interferência etc. Mas, definitivamente, mostra que era difícil até então.
Luaan 2/09
1
Boa resposta, embora eu queira dizer que você não precisa contar o número de ciclos para cada instrução no Atari 2600. Ele possui dois recursos para ajudá-lo a não precisar fazer isso: Um contador regressivo que você inicializa e faça uma sondagem para verificar se atingiu 0 e um registro que interrompe a CPU até o próximo apagamento horizontal começar. Muitos outros dispositivos, como o ZX Spectrum, não têm nada parecido, e você realmente precisa contar todos os ciclos passados ​​após a interrupção do apagamento vertical para saber onde está na tela.
Martin Vilcans
1
Eu argumentaria que o problema da parada não se aplica estritamente ao Atari. Se você excluir os recursos de E / S do Atari e restringi-lo a uma ROM de cartucho típica, haverá uma quantidade finita de armazenamento. Nesse ponto, você possui uma máquina de estados finitos; portanto, qualquer programa nele deve parar ou entrar no estado em que entrou anteriormente, levando a um loop infinito comprovável em tempo finito.
user1937198 2/09
1
@ user1937198 128 bytes de estado (mais o que estiver nos registros) é MAIS do que suficiente espaço de estado para fazer a diferença entre isso e a fita infinita teórica da máquina de Turing uma distinção que só importa em teoria. Inferno, não podemos praticamente pesquisar os 128 bits de algo como uma chave AES ... O espaço de estado cresce assustadoramente rápido à medida que você adiciona bits. Não esqueça que o equivalente a 'Desativar interrompe; parar seria quase certamente possível.
Dan Mills
1
"esse é o problema da parada, que se provou insolúvel. Se você se deparar com isso, precisará interromper o cronômetro e executar seu código". - isso não faz sentido. Você não pode escapar da prova de Turing "executando" o código em vez de simulá-lo. Se parar, você pode determinar quanto tempo leva para parar. Se não parar, você nunca pode ter certeza (em geral) se isso irá parar no futuro ou durará para sempre. É o mesmo problema com um cronômetro real ou simulado. Pelo menos em uma simulação, você pode inspecionar mais facilmente o estado interno quanto a sinais de loop.
benrg 3/09
15

Há dois aspectos em jogo aqui

Como @ gnasher729 aponta, se sabemos as instruções exatas a serem executadas, ainda é difícil estimar o tempo de execução exato por causa de coisas como cache, previsão de ramificação, dimensionamento etc.

No entanto, a situação é ainda pior. Dado um pedaço de montagem, é impossível saber quais instruções serão executadas ou mesmo quantas instruções serão executadas. Isso se deve ao teorema de Rice: se pudéssemos determinar isso com precisão, poderíamos usar essas informações para resolver o problema da parada, o que é impossível.

O código de montagem pode conter saltos e ramificações, o que é suficiente para tornar o rastreamento completo de um programa possivelmente infinito. Houve um trabalho sobre aproximações conservadoras do tempo de execução, que fornece limites superiores à execução, por meio de coisas como semântica de custos ou sistemas de tipos anotados. Não estou familiarizado com nada específico para montagem, mas não ficaria surpreso se algo assim existisse.

jmite
fonte
4
Quero dizer, o Problema de Parada se aplica diretamente aqui, pois se soubéssemos o tempo de execução saberíamos se ele parava. Também o fato de não haver condicionais nem ajuda aqui, já que no x86 mové Turing-Complete
BlueRaja - Danny Pflughoeft
7
Rice e o Problema da Parada são declarações sobre programas arbitrários (quaisquer) - mas o OP aqui especificou uma parte específica do código na pergunta. Você pode determinar propriedades semânticas e de interrupção sobre categorias individuais ou limitadas de programas, certo? Só que não há procedimento geral que cubra todos os programas.
Daniel R. Collins
2
Definitivamente, podemos saber qual instrução será executada a seguir, o que não podemos dizer é se alguma vez tocamos em um sys_exite, assim, paramos o cronômetro. Se nos restringirmos ao encerramento de programas, o que é razoável para uma pergunta tão prática, então a resposta será realmente sim (desde que você tenha um instantâneo perfeito do estado, hw e sw, do sistema imediatamente antes de iniciar o programa).
Margaret Bloom
1
O @ BlueRaja-DannyPflughoeft Mov está completo, mas não no código que o OP tem aqui. Mas isso está além do ponto de qualquer maneira - os ints podem executar código arbitrário, aguardar operações arbitrárias de E / S, etc.
Luaan
2

A escolha do "sistema de computador" incluiria microcontroladores? Alguns microcontroladores têm tempos de execução muito previsíveis, por exemplo, a série PIC de 8 bits possui quatro ciclos de clock por instrução, a menos que a instrução se ramifique para um endereço diferente, leia do flash ou seja uma instrução especial de duas palavras.

As interrupções interromperão obviamente esse tipo de timimg, mas é possível fazer muito sem um manipulador de interrupções em uma configuração "bare metal".

Usando assembly e um estilo de codificação especial, é possível escrever um código que sempre levará o mesmo tempo para ser executado. Não é tão comum agora que a maioria das variantes PIC possui vários temporizadores, mas é possível.

Oliver Broad
fonte
2

Na era dos computadores de 8 bits, alguns jogos faziam algo assim. Os programadores usariam a quantidade exata de tempo necessária para executar as instruções, com base na quantidade de tempo que levavam e na velocidade de clock conhecida da CPU, para sincronizar com os horários exatos do hardware de vídeo e áudio. Naquela época, a tela era um monitor de tubo de raios catódicos que percorria cada linha da tela a uma taxa fixa e pintava essa linha de pixels ativando e desativando o raio catódico para ativar ou desativar os fósforos. Como os programadores precisavam dizer ao hardware de vídeo o que exibir logo antes que o feixe chegasse àquela parte da tela e encaixar o restante do código no tempo que restasse, eles chamavam de "corrida no feixe".

Absolutamente não funcionaria em nenhum computador moderno ou em códigos como o seu exemplo.

Por que não? Aqui estão algumas coisas que atrapalhariam o tempo simples e previsível:

A velocidade da CPU e as buscas de memória são gargalos no tempo de execução. É um desperdício de dinheiro executar uma CPU mais rapidamente do que pode buscar instruções para executar ou instalar memória que pode fornecer bytes mais rapidamente do que a CPU pode aceitá-los. Por esse motivo, os computadores antigos rodavam no mesmo relógio. As CPUs modernas funcionam muito mais rápido que a memória principal. Eles gerenciam isso com caches de instruções e dados. A CPU continuará paralisada se precisar esperar por bytes que não estão no cache. As mesmas instruções serão executadas muito mais rapidamente se elas já estiverem no cache do que se não estiverem.

Além disso, as CPUs modernas têm pipelines longos. Eles mantêm seu alto rendimento, fazendo com que outra parte do chip faça um trabalho preliminar nas próximas instruções no pipeline. Isso falhará se a CPU não souber qual será a próxima instrução, o que pode acontecer se houver uma ramificação. Portanto, as CPUs tentam prever saltos condicionais. (Você não possui nenhum trecho de código, mas talvez tenha havido um salto condicional imprevisto que entupiu o pipeline. Além disso, boa desculpa para vincular essa resposta lendária.) Da mesma forma, os sistemas que chamam int 80para interceptar o modo kernel realmente estão usando um recurso complicado da CPU, um portão de interrupção, que introduz um atraso imprevisível.

Se o seu sistema operacional usar multitarefa preemptiva, o encadeamento executando esse código poderá perder seu tempo a qualquer momento.

A corrida na trave também funcionava porque o programa estava sendo executado apenas no metal e batia diretamente no hardware. Aqui, você está ligando int 80para fazer uma chamada do sistema. Isso passa o controle para o sistema operacional, o que não oferece garantia de tempo. Você então diz para fazer E / S em um fluxo arbitrário, que pode ter sido redirecionado para qualquer dispositivo. É muito abstrato para você dizer quanto tempo a E / S leva, mas certamente dominará o tempo gasto na execução das instruções.

Se você deseja um tempo exato em um sistema moderno, é necessário introduzir um loop de atraso. Você precisa executar as iterações mais rápidas na velocidade mais lenta, não sendo possível o inverso. Uma das razões pelas quais as pessoas fazem isso no mundo real é impedir o vazamento de informações criptográficas para um invasor que pode determinar quais solicitações demoram mais do que outras.

Davislor
fonte
1

Isso é um pouco tangencial, mas o ônibus espacial tinha 4 computadores redundantes que dependiam de uma sincronização precisa, ou seja, exatamente o tempo de execução correspondente.

A primeira tentativa de inicialização do ônibus espacial foi eliminada quando o computador do Backup Flight Software (BFS) se recusou a sincronizar com os quatro computadores PASS (Primary Avionics Software System). Detalhes em "The Bug Heard Round the World" aqui . Leitura fascinante sobre como o software foi desenvolvido para combinar ciclo por ciclo e pode fornecer informações interessantes.

Edgar H
fonte
0

Acho que estamos misturando duas questões diferentes aqui. (E sim, eu sei que isso foi dito por outras pessoas, mas espero poder expressá-lo mais claramente.)

Primeiro, precisamos ir do código-fonte para a sequência de instruções que é realmente executada (que precisa de conhecimento dos dados de entrada e do código - quantas vezes você percorre um loop? Qual ramo é executado após um teste? ) Por causa do problema de parada, a sequência de instruções pode ser infinita (sem terminação) e nem sempre é possível determinar isso estaticamente, mesmo com o conhecimento dos dados de entrada.

Depois de estabelecer a sequência de instruções a serem executadas, você deseja determinar o tempo de execução. Isso certamente pode ser estimado com algum conhecimento da arquitetura do sistema. Mas o problema é que, em muitas máquinas modernas, o tempo de execução depende muito do armazenamento em cache de buscas de memória, o que significa que depende tanto dos dados de entrada quanto das instruções executadas. Também depende da estimativa correta dos destinos de ramificação condicional, que dependem novamente dos dados. Portanto, será apenas uma estimativa, não será exato.

Michael Kay
fonte