Como os idiomas influenciaram o design da CPU? [fechadas]


44

Muitas vezes nos dizem que o hardware não se importa em qual idioma o programa está escrito, pois apenas vê o código binário compilado, mas essa não é a verdade. Por exemplo, considere o humilde Z80; suas extensões ao conjunto de instruções 8080 incluem instruções como CPIR, que são úteis para verificar seqüências de caracteres no estilo C (terminação NULL), por exemplo, para executar strlen(). Os designers devem ter identificado que a execução de programas em C (ao contrário de Pascal, onde o comprimento de uma string está no cabeçalho) era algo para o qual o design provavelmente seria usado. Outro exemplo clássico é o Lisp Machine .

Que outros exemplos existem? Por exemplo, instruções, número e tipo de registros , modos de endereçamento, que fazem um determinado processador favorecer as convenções de um idioma específico? Estou particularmente interessado em revisões da mesma família.


3
Não se esqueça que o Z-80 também possui a instrução LDIR, muito útil ao copiar strings quando você conhece o comprimento (como em Pascal, onde o comprimento estava armazenado no cabeçalho).
TMN

27
1. O Z-80 foi projetado em 1975, quando Unix e C eram um sistema operacional e idioma obscuros em alguns computadores, três anos antes da primeira edição da K&R. 2. Não há nada sobre Pascal que exija que o comprimento da string esteja "em um cabeçalho". 3. As seqüências de caracteres no CP / M, o principal sistema operacional do microcomputador da época, foram finalizadas com o caractere '$', não '\ 0'. O CPIR pode procurar qualquer caractere. 4. O CPIR é correspondido ao CPDR (pesquisa para trás), bem como outras instruções -IR e -DR. Conclusão: O CPIR não tem nada a ver com a linguagem de programação C. É apenas uma instrução de pesquisa de bytes.
Librik 31/07/12

4
A maior (e uma das mais irritantes para os projetistas de hardware) das coisas forçadas por C é um endereçamento de bytes. As CPUs teriam sido mais simples e rápidas sem essa abominação.
SK-logic

1
@ SK-logic: Embora o padrão POSIX exija endereçamento de bytes, o padrão C não. Qualquer implementação em que sizeof(int)seja igual a 1 deve exigir que o tipo charseja assinado (já que um intdeve poder manter todos os valores do tipo char). Eu escrevi código para uma máquina onde chare intsão números inteiros assinados de 16 bits; as maiores dificuldades são que não se pode usar uniões para conversão de tipos, e o armazenamento eficiente de um grande número de bytes requer empacotamento e descompactação manuais. Essas questões são menores em comparação com a possibilidade em C de que sizeof (int) == sizeof (long), desde que ... #
1113

2
... isso significa que não há um tipo padrão que garanta a diferença entre dois unsigned intvalores. O C99 melhorou essa situação, mas antes do C99 não havia uma maneira segura garantida de comparar um valor potencialmente negativo a um valor do tipo unsigned int(seria necessário testar se o número era negativo antes de fazer a comparação).
Supercat 01/08

Respostas:


20

As respostas existentes se concentram nas mudanças do ISA . Também há outras alterações de hardware. Por exemplo, o C ++ geralmente usa vtables para chamadas virtuais. Começando com o Pentium M , a Intel possui um componente "preditor de ramificação indireta" que acelera as chamadas de função virtual.


6
E a arquitetura RISC de Berkeley incluía o conceito de um "arquivo de registro"; portanto, em vez de fazer as funções "derramarem" registros na pilha, um bloco de 8 registros era fornecido para cada função. Isso acelerou consideravelmente o código orientado a objetos, uma vez que tende a consistir em muitas chamadas de métodos para métodos curtos.
TMN

1
Este não é um exemplo válido. O design da "Tabela de ponteiros de função" também é usado em muitos cenários de vinculação dinâmica, por exemplo, através da importação e exportação de DLL no Windows e também usado em programas em C. Embora eu ache que você possa argumentar que mostra que o processador está sendo otimizado para um uso específico, não é específico do idioma.
DeadMG

@DeadMG: Outros casos beneficiados, isso é verdade. Mas até o C ++ se tornar popular, os designs de CPU não foram influenciados . E essa foi a questão colocada. Da mesma forma, a TMN tem razão sobre os arquivos de registro. Assembly não tinha um conceito tão claro de funções. As funções, como as conhecemos hoje em dia, remontam ao Algol 60 e, portanto, podemos dizer que o Algol 60 influenciou o design do arquivo de registro da CPU.
MSalters

14

O conjunto de instruções Intel 8086 inclui uma variação de "ret", que adiciona um valor ao ponteiro da pilha após a inserção do endereço de retorno. Isso é útil para muitas implementações do Pascal, nas quais o chamador de uma função envia argumentos para a pilha antes de fazer uma chamada de função e os expande posteriormente. Se uma rotina aceitar, por exemplo, parâmetros de quatro bytes, ela poderá terminar com "RET 0004" para limpar a pilha. Na ausência de tal instrução, uma convenção de chamada provavelmente exigiria que o código localizasse o endereço de retorno em um registro, atualizasse o ponteiro da pilha e depois pulasse para esse registro.

Curiosamente, a maioria dos códigos (incluindo rotinas do SO) no Macintosh original usava a convenção de chamada Pascal, apesar da falta de uma instrução facilitadora no 68000. O uso dessa convenção de chamada economizou 2-4 bytes de código em um site de chamada típico, mas exigia um extra 4-6 bytes de código no site de retorno de todas as funções que utilizaram parâmetros.


Há também uma ENTERcontrapartida para isso RET n... #
6131

1
@herby: Eu acho que não ENTERexistia no original 8086; veio com processadores posteriores. Porém, traz um ponto interessante: os modos de endereçamento baseados em BP são claramente projetados em torno do uso de parâmetros empilhados e locais acessados ​​via ponteiro de quadro. Acho essa convenção interessante de várias maneiras, especialmente considerando que (1) o código puro da linguagem assembly é mais apto a usar valores em registradores do que a pilha, mas (2) as vantagens de abordar [BP + nn] sobre [SP + nn] o endereçamento é mais significativo para programas em linguagem assembly que acessam coisas na pilha do que ... #
31812

... para código de montagem escrito à mão. Um compilador geralmente saberá, para cada instrução gerada, como o SP e o BP se comparam; se SP for BP-8, por exemplo, não é realmente mais fácil para o compilador abordar [BP + 12] do que [SP + 20]. Se, em uma recompilação, o compilador precisar adicionar outro PUSH / POP em torno de um bloco de código, ele poderá ajustar apropriadamente as compensações baseadas em SP. Por outro lado, em uma montagem escrita à mão, adicionar um PUSH / POP provavelmente exigiria ajustar o código entre eles. Portanto, os ponteiros de quadro são principalmente um benefício para o código combinado de alto nível / asm.
Supercat 01/08

Talvez a possibilidade de reutilizar o código sem sua recompilação também seja um ponto de usabilidade marginal para o endereçamento da BP. E Deus sabe se as instruções de endereçamento BP não mais rápido estão em circuitos de SP dirigida queridos, já que BP endereçamento é uma espécie de padrão ...
herby

3
@herby: Na verdade, suspeito que grande parte da razão pela qual os compiladores usaram ponteiros de quadros geralmente tem muito a ver com a depuração. Para depurar um programa que não usou essa convenção, seria necessário que o compilador gere - e o depurador use - um arquivo listando o deslocamento do SP-BP para cada instrução. Esses metadados detalhados são comuns hoje em dia (e são uma parte essencial do que torna as linguagens de coleta de lixo práticas), mas a quantidade de RAM necessária seria inaceitável há 30 anos.
Supercat 01/08

10

Um exemplo é o MIPS, que possui ambos adde addupara interceptar e ignorar o estouro, respectivamente. (Também sube subu.) Precisava do primeiro tipo de instrução para idiomas como Ada (eu acho - eu nunca usei o Ada) que lida com estouros de forma explícita e o segundo tipo para idiomas como C que ignoram estouros.

Se bem me lembro, a CPU real possui alguns circuitos adicionais na ALU para controlar os estouros. Se o único idioma com o qual as pessoas se importavam era o C, não seria necessário.


Não tenho certeza se está relacionado, mas essas instruções provavelmente também são úteis em outras situações, como alocação segura de memória, ou seja, se você estiver alocando nmemb*size+offsetbytes e precisar garantir que não haja um estouro.
NikiC 04/08

@ Nikik: Eu estava pensando que as instruções addue subu(as que não verificam se há estouros) foram as que foram adicionadas para deixar C feliz. Claro, eu realmente não sei - nós apenas o abordamos vagamente em palestras e eu certamente não sou especialista em arquitetura: p.
Tikhon Jelvis

Ah, sim, eu estava pensando o contrário, desculpe: /
NikiC 4/12

8

A série Burroughs 5000 foi projetada para oferecer suporte eficiente à ALGOL, e o iAPX-432 da Intel foi projetado para executar com eficiência o Ada. O Inmos Transputer tinha seu próprio idioma, Occam. Eu acho que o processador Parallax "Propeller" foi projetado para ser programado usando sua própria variante do BASIC.

Não é um idioma, mas o conjunto de instruções do VAX-11 possui uma única instrução para carregar um contexto de processo, que foi projetado após uma solicitação da equipe de design do VMS. Não me lembro dos detalhes, mas o ISTR levou tantas instruções para implementar que colocou um limite superior sério no número de processos que eles poderiam agendar.


O que há nesses designs que os torna particularmente adequados? Por exemplo, de que recurso do iAPX o Ada se beneficia particularmente?
Gaius

ISTR de que o alvo Ada do iAPX-432 estava mais tentando salvar um design com falha, anexando-o a algo com grandes expectativas ainda do que qualquer outra coisa.
AProgramador 31/07

@ AProgrammer: Tenho certeza de que o iAPX-432 foi projetado desde o início para usar o Ada. Até me lembro de alguns rumores de que a Intel não publicaria o conjunto de instruções, para desencorajar a programação em linguagem assembly e forçar as pessoas a usar Ada para tudo.
TMN

1
@TMN, o projeto 432 da Intel começou em 1975 e foi introduzido em 1981 (Wikipedia). O Ironman (requisitos finais para Ada) foi publicado em janeiro de 1977 e o verde foi escolhido em maio de 1979, modificado e o resultado final publicado como padrão militar em julho de 1980. Há um problema no cronograma ao afirmar que o iAPX-432 foi desenvolvido a partir de o começo para usar Ada. (É um processador tardio e típico "feche a lacuna semântica" com as desvantagens usuais em um momento em que as alternativas começaram a ser pesquisadas; comercializá-lo como processador Ada foi uma tentativa de salvar um projeto com falha - ISTR que ninguém além da Intel o usou )
AProgrammer

1
@ AProgrammer: Hmmm, parece que você está certo. Encontrei este artigo com o arquiteto principal do 432 e, no resumo, ele diz: "Essa estreita correspondência entre arquitetura e linguagem não ocorreu porque o 432 foi projetado para executar Ada - não era". Vou ter que desenterrar meu antigo livro 432 e ver o que ele diz.
TMN

8

Uma coisa que ninguém parece ter mencionado até agora é que os avanços na otimização do compilador (onde a linguagem base é irrelevante) levaram a mudança dos conjuntos de instruções CISC (que foram amplamente projetados para serem codificados por humanos) para os conjuntos de instruções RISC (que eram amplamente projetado para ser codificado por compiladores.)


5

A família Motorola 68000 introduziu algum modo de endereço de incremento automático que tornou a cópia de dados por meio da CPU muito eficiente e compacta.

[Exemplo atualizado]

este foi um código c ++ que influenciou 68000 assembler

while(someCondition)
    destination[destinationOffset++] = source[sourceOffset++]

implementado no assembler convencional (pseudocódigo, esqueci os comandos 68000 assembler)

adressRegister1 = source
adressRegister2 = destination
while(someCondition) {
    move akku,(adressRegister1)
    move (adressRegister2), akku
    increment(adressRegister1, 1)
    increment(adressRegister2, 1)
}

com o novo modo de endereço, tornou-se algo semelhante a

adressRegister1 = source
adressRegister2 = destination
while(someCondition) {
    move akku,(adressRegister1++)
    move (adressRegister2++), akku
}

apenas duas instruções por loop em vez de 4.


1
Como isso foi influenciado pelas convenções de um idioma específico?
Gaius

veja exemplo atualizado
k3b

Ah, me lembra a otimização de loop DBxx no 68010.
Gaius

7
Na verdade, acho que você tem isso ao contrário. O endereçamento automático de informações fazia parte do conjunto de instruções do PDP-11, o que provavelmente influenciou o design de C.
TMN

5

O mainframe da série Z da IBM é o descendente do IBM 360 a partir da década de 1960.

Houve várias instruções específicas para acelerar os programas COBOL e Fortran. O exemplo clássico é o BXLE- "Ramificação no Índice Baixo ou Igual", que é a maior parte de um forloop Fortran ou um COBOL PERFORM VARYING x from 1 by 1 until x > nencapsulado em uma única instrução.

Há também uma família inteira de instruções decimais compactadas para suportar a aritmética decimal de ponto fixo comum nos programas COBOL.


Eu acho que você quer dizer descendente .
Clockwork-Muse

@ X-Zero - oops! De manhã cedo, não caffiene suficiente no sistema etc .......
James Anderson

1
Mais interessante é a instrução de repetição de bloco da TI 32050 DSP. Seu operando é o endereço da instrução após o último no loop; carregar um registro de contagem de loop e executar a instrução de repetição de bloco fará com que instruções até (mas não incluindo) o alvo sejam repetidas o número especificado de vezes. Muito fortemente remanescente de um DOloop FORTRAN .
Supercat 01/08

@supercat Todo DSP digno desse nome inclui três recursos: loop de sobrecarga zero, instrução única de acumulação múltipla e um modo de endereçamento de bits reversos de algum tipo. Quase todo algoritmo DSP conhecido pelo Man usa loops. Os dois algoritmos mais comuns são o filtro FIR, que é um loop em torno de um acúmulo de multiplicação, e a FFT, para a qual o endereçamento reverso de bits é crítico. Muitos DSPs incluem uma operação borboleta radix-2 FFT com uma instrução ou uma multiplicação / adição dupla que pode ser usada para criar uma borboleta com uma instrução.
John R. Strohm

@ JohnR.Strohm: Todo DSP que eu vi inclui um repetir, multiplicar, acumular, mas nem todos incluem loops de sobrecarga zero mais generalizados. Na verdade, não sei ao certo por que esses loops devem ser considerados apenas um recurso "DSP", pois também seriam úteis em muitos códigos de "processador convencional".
Supercat

3

Os primeiros processadores Intel tinham os seguintes recursos, muitos deles agora obsoletos no modo de 64 bits:

  • Instruções ENTER, LEAVE e RET nn [manuais antigos informaram explicitamente que foram introduzidos para linguagens estruturadas em bloco, por exemplo, Pascal, que suporta procedimentos aninhados]
  • instruções para acelerar a aritmética do BCD (AAA, AAM, etc.); também suporte BCD em x87
  • Instruções JCXZ e LOOP para implementar loops contados
  • INTO, para gerar uma armadilha no estouro aritmético (por exemplo, no Ada)
  • XLAT para pesquisas de tabela
  • LIMITE para verificar os limites da matriz

O sinalizador de sinal, encontrado no registro de status de muitas CPUs, existe para executar facilmente aritmética assinada E não assinada.

O conjunto de instruções do SSE 4.1 apresenta instruções para o processamento de cadeias, contadas e terminadas com zero (PCMPESTR, etc.)

Além disso, eu poderia imaginar que vários recursos no nível do sistema foram projetados para oferecer suporte à segurança do código compilado (verificação de limite de segmento, portas de chamada com cópia de parâmetros etc.)


3

Alguns processadores ARM, principalmente aqueles em dispositivos móveis, incluem (d) extensão Jazelle, que é intérprete de JVM de hardware; interpreta diretamente o bytecode Java. A JVM com reconhecimento de Jazelle pode usar o hardware para acelerar a execução e eliminar grande parte do JIT, mas o fallback para a VM do software ainda é garantido se o bytecode não puder ser interpretado no chip.

Os processadores com essa unidade incluem a instrução BXJ, que coloca o processador no "modo Jazelle" especial ou, se a ativação da unidade falhou, ela é interpretada como uma instrução de ramificação normal. A unidade reutiliza os registros do ARM para manter o estado da JVM.

O sucessor da tecnologia Jazelle é o ThumbEE


2

Tanto quanto sei, isso era mais comum no passado.

uma sessão de perguntas em que James Gosling disse que havia pessoas tentando criar hardware que pudesse lidar melhor com o bytecode da JVM, mas essas pessoas descobririam uma maneira de fazê-lo com o intel x86 "genérico" comum (talvez compilando o bytecode de alguma maneira inteligente).

Ele mencionou que há vantagem em usar o popular chip genérico (como o da Intel) porque ele possui uma grande corporação que lança enormes somas de dinheiro no produto.

Vale a pena conferir o vídeo. Ele fala sobre isso aos 19 ou 20 minutos.



2

A CPU Intel iAPX foi projetada especificamente para idiomas OO. Não deu muito certo, no entanto.

O iAPX 432 ( arquitetura Intel Advanced Processor ) foi o primeiro design de microprocessador de 32 bits da Intel, lançado em 1981 como um conjunto de três circuitos integrados. Ele pretendia ser o principal projeto da Intel nos anos 80, implementando muitos recursos avançados de multitarefa e gerenciamento de memória. Portanto, o design foi referido como um Micromainframe ...

O iAPX 432 foi "projetado para ser programado inteiramente em linguagens de alto nível" , com Ada sendo primário e suportava programação orientada a objetos e coleta de lixo diretamente em hardware e microcódigo . O suporte direto a várias estruturas de dados também visava permitir que os sistemas operacionais modernos para o iAPX 432 fossem implementados usando muito menos código de programa do que os processadores comuns. Essas propriedades e recursos resultaram em um design de hardware e microcódigo muito mais complexo do que a maioria dos processadores da época, especialmente os microprocessadores.

Usando a tecnologia de semicondutores de sua época, os engenheiros da Intel não conseguiram traduzir o design em uma primeira implementação muito eficiente. Juntamente com a falta de otimização em um compilador Ada prematuro, isso contribuiu para sistemas de computador bastante lentos, mas caros, executando benchmarks típicos a aproximadamente 1/4 da velocidade do novo chip 80286 na mesma freqüência de clock (no início de 1982).

Essa brecha no desempenho inicial do perfil bastante baixo e da linha 8086 de baixo preço foi provavelmente a principal razão pela qual o plano da Intel de substituir o último (mais tarde conhecido como x86) pelo iAPX 432 falhou. Embora os engenheiros tenham visto maneiras de melhorar o design da próxima geração, a arquitetura do iAPX 432 Capability havia começado a ser vista mais como uma sobrecarga de implementação do que como o suporte simplificador que pretendia ser.

O projeto iAPX 432 foi um fracasso comercial da Intel ...


Lendo o artigo, parece que muitos aspectos do design podem ser úteis em estruturas orientadas a objetos, como são populares atualmente. Uma arquitetura que usou uma combinação de um ID de objeto de 32 bits e um deslocamento de 32 bits poderia, em muitos casos, oferecer melhor desempenho de armazenamento em cache do que aquele em que os IDs de objetos tinham todos os 64 bits (na maioria dos casos, um aplicativo que usaria bilhões de objetos ser mais bem servidos por, em vez de ter mais, maiores; um que seria armazenar milhares de milhões de bytes de um objecto seria melhor servidos subdividindo que em objectos menores.
supercat

1

O 68000 possuía o MOVEM, que era mais adequado para inserir vários registros na pilha em uma única instrução, o que muitos idiomas esperavam.

Se você viu o MOVEM (MOVE Multiple) precedendo o JSR (Jump SubRotine) em todo o código, geralmente sabia que estava lidando com o código C em conformidade.

O MOVEM permitiu o incremento automático do registro de destino, permitindo que cada uso continue empilhando no destino ou removendo da pilha no caso de decremento automático.

http://68k.hax.com/MOVEM


1

A arquitetura AVR da Atmel foi totalmente projetada desde o início para ser adequada para programação em C. Por exemplo, esta nota de aplicação é mais detalhada.

Na IMO, isso está intimamente relacionado à excelente resposta do rockets4kids , com PIC16-s iniciais sendo desenvolvidos para programação direta de montadores (40 instruções no total), com famílias mais tarde mirando C.


1

Quando o coprocessador numérico 8087 foi projetado, era bastante comum que os idiomas realizassem todas as matemáticas de ponto flutuante usando o tipo de maior precisão e arredondassem o resultado para menor precisão ao atribuí-lo a uma variável de menor precisão. No padrão C original, por exemplo, a sequência:

float a = 16777216, b = 0.125, c = -16777216;
float d = a+b+c;

promoveria ae bpara double, adicioná-los, promover ca double, adicioná-lo, e depois armazenar o resultado arredondado para float. Embora em muitos casos fosse mais rápido para um compilador gerar código que executasse operações diretamente no tipo float, era mais simples ter um conjunto de rotinas de ponto flutuante que operariam apenas no tipo double, além de rotinas para converter em / from float, do que ter conjuntos separados de rotinas para lidar com operações em floate double. O 8087 foi projetado em torno dessa abordagem da aritmética, executando todas as operações aritméticas usando um tipo de ponto flutuante de 80 bits [80 bits provavelmente foi escolhido porque:

  1. Em muitos processadores de 16 e 32 bits, é mais rápido trabalhar com uma mantissa de 64 bits e um expoente separado do que trabalhar com valor que divide um byte entre a mantissa e o expoente.

  2. É muito difícil realizar cálculos precisos com a precisão total dos tipos numéricos que se está usando; se alguém está tentando, por exemplo, calcular algo como log10 (x), é mais fácil e rápido calcular um resultado com precisão de até 100ulp de um tipo de 80 bits do que calcular um resultado com precisão de 1ulp de 64 bits tipo, e arredondar o resultado anterior para a precisão de 64 bits produzirá um valor de 64 bits mais preciso que o último.

Infelizmente, versões futuras da linguagem mudaram a semântica de como os tipos de ponto flutuante deveriam funcionar; enquanto a semântica do 8087 teria sido muito boa se as linguagens as suportassem consistentemente, se as funções f1 (), f2 () etc. retornassem tipo float, muitos autores do compilador se encarregariam de criar long doubleum alias para o tipo duplo de 64 bits em vez do tipo de 80 bits do compilador (e não fornece outros meios de criar variáveis ​​de 80 bits) e avaliar arbitrariamente algo como:

double f = f1()*f2() - f3()*f4();

de qualquer uma das seguintes maneiras:

double f = (float)(f1()*f2()) - (extended_double)f3()*f4();
double f = (extended_double)f1()*f2() - (float)(f3()*f4());
double f = (float)(f1()*f2()) - (float)(f3()*f4());
double f = (extended_double)f1()*f2() - (extended_double)f3()*f4();

Observe que, se f3 e f4 retornam os mesmos valores que f1 e f2, respectivamente, a expressão original deve retornar claramente zero, mas muitas dessas expressões podem não retornar. Isso levou as pessoas a condenar a "precisão extra" do 8087, embora a última formulação geralmente fosse superior à terceira e - com código que usasse o tipo duplo estendido adequadamente - raramente seria inferior.

Nos anos seguintes, a Intel respondeu à tendência da linguagem (lamentável IMHO) de forçar resultados intermediários a serem arredondados para a precisão dos operandos, projetando seus processadores posteriores de modo a favorecer esse comportamento, em detrimento do código que se beneficiaria do uso de alto desempenho. precisão em cálculos intermediários.


Observe que você já tem uma resposta ( acima ) nesta postagem. São respostas que podem / devem ser mescladas em uma?

@ MichaelT: Eu acho que não - um abrange o design da pilha e o outro abrange a semântica de ponto flutuante.
Supercat

Apenas certificando-se. Pessoalmente, acredito que seria possível dar uma resposta mais forte (usando cabeçalhos para separar as seções), mas essa é a minha opinião. Você ainda pode usar os cabeçalhos para identificar claramente na parte superior o que cada parte da resposta aborda ( ## How the stack changed the processore ## How floating point changed the processor) para que as pessoas possam ter a mentalidade correta ao lê-la e são menos propensas a pensar que você estava distraído ao responder ou reposicionar o mesmas respostas (semelhantes).

@ MichaelT: As duas respostas são suficientemente desassociadas que acho que deveriam ser votadas separadamente. Embora o 80486 tenha absorvido as funções anteriormente desempenhadas pelo 8087/80287/80387, o 8086 e o ​​8087 foram projetados como chips separados com arquiteturas quase independentes. Embora ambos executassem o código a partir de um fluxo de instruções comum, o 8086 tratava certas sequências de bytes como solicitações para gerar solicitações de leitura / gravação de endereço enquanto ignorava o barramento de dados e o 8087 ignorava tudo o que estava acontecendo.
Supercat #
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.