Qual é a diferença de desempenho entre números inteiros não assinados e assinados? [fechadas]

42

Estou ciente do impacto no desempenho ao misturar entradas assinadas com flutuadores.

É pior misturar ints não assinadas com flutuadores?

Existe algum acerto ao misturar assinados / não assinados sem carros alegóricos?

Os diferentes tamanhos (u32, u16, u8, i32, i16, i8) afetam o desempenho? Em quais plataformas?

Luis
fonte
2
Eu removi o texto / tag específico do PS3, porque essa é uma boa pergunta sobre qualquer arquitetura, e a resposta é verdadeira para todas as arquiteturas que separam registros inteiros e de ponto flutuante, que são praticamente todos eles.

Respostas:

36

A grande penalidade de misturar ints (de qualquer tipo) e floats é porque eles estão em conjuntos de registros diferentes. Para ir de um registro definido para o outro, é necessário gravar o valor na memória e lê-lo novamente, o que implica uma paralisação de carga e armazenamento .

Alternar entre tamanhos diferentes ou assinatura de entradas mantém tudo no mesmo conjunto de registros, para evitar a grande penalidade. Pode haver penalidades menores devido a extensões de sinal etc., mas são muito menores do que uma loja de acerto de carga.

celion
fonte
O artigo que você vinculou afirma que o Processador de células PS3 é uma exceção, pois aparentemente tudo é armazenado no mesmo conjunto de registros (pode ser encontrado aproximadamente no meio do artigo ou procure por "Célula").
bummzack
4
@bummzack: isso se aplica apenas às SPEs, não ao EPI; as SPEs têm um ambiente de ponto flutuante muito especial e o elenco ainda é relativamente caro. Além disso, os custos ainda são os mesmos para números inteiros assinados versus não assinados.
Esse é um bom artigo e é importante saber sobre o LHS (e estou votando nisso), mas minha pergunta é sobre as penalidades relacionadas a sinais. Sei que estes são pequenos e provavelmente insignificantes, mas ainda gostaria de ver alguns números ou referências reais sobre eles.
Luis
1
@Luis - Eu estava tentando encontrar alguma documentação pública sobre isso, mas não consigo encontrá-la no momento. Se você tiver acesso à documentação do Xbox360, há um bom white paper de Bruce Dawson que cobre parte disso (e é muito bom em geral).
celion
@Luis: Eu publiquei uma análise abaixo, mas se isso lhe satisfizer, dê a resposta a Celion - tudo o que ele disse está correto, tudo o que fiz foi executar o GCC algumas vezes.
12

Eu suspeito que as informações sobre o Xbox 360 e PS3 especificamente estejam atrás de muros somente para desenvolvedores licenciados, como a maioria dos detalhes de baixo nível. No entanto, podemos construir um programa x86 equivalente e desmontá-lo para ter uma idéia geral.

Primeiro, vamos ver quais custos de alargamento não assinados:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

A parte relevante é desmontada (usando o GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

Então basicamente o mesmo - em um caso, movemos um byte, no outro, movemos uma palavra. Próximo:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

Torna-se em:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

Portanto, o custo da extensão do sinal é qualquer que seja o custo, movsble não movzblo nível de sub-instrução. Isso é basicamente impossível de quantificar nos processadores modernos devido à maneira como os processadores modernos funcionam. Todo o resto, variando da velocidade da memória ao cache, até o que estava no pipeline de antemão, vai dominar o tempo de execução.

Nos ~ 10 minutos que levei para escrever esses testes, eu poderia facilmente encontrar um bug de desempenho real e, assim que ligo qualquer nível de otimização do compilador, o código fica irreconhecível para tarefas simples.

Isso não é Stack Overflow, por isso espero que ninguém aqui afirme que a microoptimização não importa. Os jogos geralmente trabalham com dados muito grandes e muito numéricos; portanto, uma atenção cuidadosa às ramificações, projeções, agendamento, alinhamento de estrutura etc. pode fornecer melhorias muito críticas. Qualquer pessoa que tenha passado muito tempo otimizando o código PPC provavelmente tem pelo menos uma história de horror sobre lojas de carregamento de itens atingidos. Mas neste caso, isso realmente não importa. O tamanho do armazenamento do seu tipo inteiro não afeta o desempenho, desde que esteja alinhado e caiba em um registro.

user744
fonte
2
(CW, porque isso é realmente apenas um comentário sobre a resposta de celion, e porque eu sou curioso alterações que o código pessoas podem ter para torná-lo mais ilustrativo.)
As informações sobre a CPU do PS3 estão prontamente e legalmente disponíveis; portanto, a discussão sobre as coisas da CPU relacionadas ao PS3 não é um problema. Até a Sony remover o suporte ao OtherOS, qualquer um podia colocar o Linux em um PS3 e programá-lo. A GPU estava fora dos limites, mas a CPU (incluindo as SPEs) está bem. Mesmo sem o suporte ao OtherOS, você pode facilmente pegar o GCC apropriado e ver como é a geração de código.
JasonD 01/02/11
@ Jason: Eu sinalizei minha postagem como CW, portanto, se alguém fizer isso, poderá fornecer as informações. No entanto, qualquer pessoa com acesso ao compilador GameOS oficial da Sony - que é realmente o único que importa - provavelmente está impedida de fazê-lo.
Na verdade, o número inteiro assinado é mais caro no PPC IIRC. Ele tem um pequeno impacto no desempenho, mas está lá ... também muitos detalhes do PS3 PPU / SPU estão aqui: jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html e aqui: jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html . Curioso o que é esse compilador GameOS? Esse é o compilador GCC ou o SNC? iirc, além das coisas mencionadas, as comparações assinadas têm uma sobrecarga quando se fala em otimizar loops mais internos. Eu não tenho acesso aos documentos que descrevem esta embora - e mesmo que eu fiz ...
jheriko
4

Operações inteiras assinadas podem ser mais caras em quase todas as arquiteturas. Por exemplo, a divisão por uma constante é mais rápida quando não assinada, por exemplo:

unsigned foo(unsigned a) { return a / 1024U; }

será otimizado para:

unsigned foo(unsigned a) { return a >> 10; }

Mas...

int foo(int a) { return a / 1024; }

otimizará para:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;
}

ou em sistemas onde a ramificação é barata,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;
}

O mesmo vale para o módulo. Isso também vale para não-potências-de-2 (mas o exemplo é mais complexo). Se sua arquitetura não possui uma divisão de hardware (por exemplo, a maioria dos ARM), as divisões não assinadas de não consts também são mais rápidas.

Em geral, dizer ao compilador que números negativos não podem resultar ajudará na otimização de expressões, especialmente aquelas usadas para finalização de loop e outras condicionais.

Quanto a diferentes tamanhos de entrada, sim, há um leve impacto, mas você teria que pesar esse valor vs movimentar menos memória. Atualmente, você provavelmente ganha mais acessando menos memória do que perde com a expansão de tamanho. Você está muito longe da micro-otimização nesse ponto.

John Ripley
fonte
Editei seu código otimizado para refletir melhor o que o GCC realmente gera, mesmo em -O0. Ter um ramo era enganoso quando um teste + lea permite fazê-lo sem ramo.
2
No x86, talvez. No ARMv7, é executado apenas condicionalmente.
John Ripley
3

As operações com int assinado ou não assinado têm o mesmo custo nos processadores atuais (x86_64, x86, powerpc, arm). No processador de 32 bits, u32, u16, u8 s32, s16, s8 devem ser os mesmos. Você pode ter penalidade com mau alinhamento.

Mas converter int para flutuar ou flutuar para int é uma operação cara. Você pode encontrar facilmente a implementação otimizada (SSE2, Neon ...).

O ponto mais importante é provavelmente o acesso à memória. Se seus dados não couberem no cache L1 / L2, você perderá mais ciclo do que conversão.

Ellis
fonte
2

Jon Purdy diz acima (não posso comentar) que o não assinado pode ser mais lento porque não pode exceder. Eu discordo, aritmética não assinada é simples módulo aritmético moular 2 para o número de bits na palavra. As operações assinadas, em princípio, podem sofrer estouros, mas geralmente são desativadas.

Às vezes, você pode fazer coisas inteligentes (mas não muito legíveis), como agrupar dois ou mais itens de dados em um int e obter várias operações por instrução (aritmética de bolso). Mas você precisa entender o que está fazendo. Claro que a MMX permite que você faça isso naturalmente. Mas, às vezes, usar o maior tamanho de palavra suportado por HW e compactar manualmente os dados fornece a implementação mais rápida.

Tenha cuidado com o alinhamento de dados. Na maioria das implementações de HW, as cargas e lojas não alinhadas são mais lentas. Alinhamento natural, significa que, por exemplo, uma palavra de 4 bytes, o endereço é um múltiplo de quatro e os endereços da palavra de oito bytes devem ser múltiplos de oito bytes. Isso é transferido para o SSE (128 bits favorece o alinhamento de 16 bytes). Em breve, o AVX estenderá esses tamanhos de registro "vetorial" para 256 bits e depois para 512 bits. E as cargas / lojas alinhadas serão mais rápidas que as desalinhadas. Para os geeks de HW, uma operação de memória desalinhada pode abranger coisas como o cacheline e até os limites de página, para os quais o HW precisa ter cuidado.


fonte
1

É um pouco melhor usar números inteiros assinados para índices de loop, porque o estouro assinado não é definido em C; portanto, o compilador assumirá que esses loops têm menos casos de canto. Isso é controlado pelo "-fstrict-overflow" do gcc (ativado por padrão) e provavelmente é difícil notar o efeito sem ler a saída do assembly.

Além disso, o x86 funciona melhor se você não misturar tipos, porque pode usar operandos de memória. Se precisar converter tipos (sinal ou zero extensões), isso significa uma carga explícita e o uso de um registro.

Fique com int para variáveis ​​locais e a maior parte disso acontecerá por padrão.

alex estranho
fonte
0

Como Celion aponta, a sobrecarga de conversão entre entradas e flutuadores tem a ver com a cópia e conversão de valores entre registros. A única sobrecarga de entradas não assinadas por si só vem de seu comportamento abrangente garantido, que requer uma certa quantidade de verificação de sobrecarga no código compilado.

Basicamente, não há sobrecarga na conversão entre números inteiros assinados e não assinados. Tamanhos diferentes de número inteiro podem ser (infinitesimalmente) mais rápidos ou mais lentos para acessar, dependendo da plataforma. De um modo geral, o tamanho do número inteiro mais próximo do tamanho da palavra da plataforma será o mais rápido de acessar, mas a diferença geral de desempenho depende de muitos outros fatores, principalmente do tamanho do cache: se você usar uint64_tquando tudo o que precisar uint32_t, poderá menos dados caberão no cache de uma só vez e você poderá sofrer uma sobrecarga de carga.

É um pouco excessivo pensar nisso, no entanto. Se você usa tipos apropriados para seus dados, as coisas devem funcionar perfeitamente bem, e a quantidade de energia a ser obtida pela seleção de tipos baseados na arquitetura é desprezível.

Jon Purdy
fonte
A que verificação de estouro você está se referindo? A menos que você queira dizer um nível mais baixo que o assembler, o código para adicionar duas entradas é idêntico na maioria dos sistemas e não muito mais nos poucos que usam, por exemplo, magnitude do sinal. Apenas diferente.
@JoeWreschnig: Droga. Parece que não consigo encontrá-lo, mas sei que já vi exemplos de diferentes resultados de assembler que explicam o comportamento abrangente, pelo menos em determinadas plataformas. O único post relacionado que eu pude encontrar: stackoverflow.com/questions/4712315/…
Jon Purdy
Uma saída diferente do assembler para um comportamento diferente envolve porque o compilador pode fazer otimizações no caso assinado que, por exemplo, se b> 0 então a + b> a, porque o estouro assinado é indefinido (e, portanto, não se pode confiar). É realmente uma situação totalmente diferente.