A maneira mais rápida de determinar se um número inteiro está entre dois números inteiros (inclusive) com conjuntos de valores conhecidos

389

Existe uma maneira mais rápida do que x >= start && x <= endem C ou C ++ para testar se um número inteiro está entre dois números inteiros?

ATUALIZAÇÃO : minha plataforma específica é iOS. Isso faz parte de uma função de desfoque de caixa que restringe os pixels a um círculo em um determinado quadrado.

ATUALIZAÇÃO : Depois de tentar a resposta aceita , recebi um aumento de ordem de magnitude na única linha de código ao fazê-lo da x >= start && x <= endmaneira normal .

UPDATE : Aqui está o código depois e antes com o assembler do XCode:

NEW WAY

// diff = (end - start) + 1
#define POINT_IN_RANGE_AND_INCREMENT(p, range) ((p++ - range.start) < range.diff)

Ltmp1313:
 ldr    r0, [sp, #176] @ 4-byte Reload
 ldr    r1, [sp, #164] @ 4-byte Reload
 ldr    r0, [r0]
 ldr    r1, [r1]
 sub.w  r0, r9, r0
 cmp    r0, r1
 blo    LBB44_30

À MODA ANTIGA

#define POINT_IN_RANGE_AND_INCREMENT(p, range) (p <= range.end && p++ >= range.start)

Ltmp1301:
 ldr    r1, [sp, #172] @ 4-byte Reload
 ldr    r1, [r1]
 cmp    r0, r1
 bls    LBB44_32
 mov    r6, r0
 b      LBB44_33
LBB44_32:
 ldr    r1, [sp, #188] @ 4-byte Reload
 adds   r6, r0, #1
Ltmp1302:
 ldr    r1, [r1]
 cmp    r0, r1
 bhs    LBB44_36

É incrível como reduzir ou eliminar a ramificação pode proporcionar uma velocidade tão dramática.

jjxtra
fonte
28
Por que você está preocupado que isso não seja rápido o suficiente para você?
Matt Bola
90
Quem se importa com isso, é uma pergunta interessante. É apenas um desafio por um desafio.
David diz Reinstate Monica
46
@SLaks Portanto, devemos simplesmente ignorar todas essas perguntas cegamente e apenas dizer "deixe o otimizador fazer isso?"
David diz Reinstate Monica
87
não importa por que a pergunta está sendo feita. É uma pergunta válida, mesmo que a resposta seja não
Tay10r 13/06/2013
41
Este é um gargalo em uma função em um dos meus aplicativos
jjxtra

Respostas:

527

Há um velho truque para fazer isso com apenas uma comparação / ramificação. Se isso realmente melhora a velocidade pode ser questionável, e mesmo que isso aconteça, é provavelmente muito pouco para se notar ou se preocupar, mas quando você está começando apenas com duas comparações, as chances de uma grande melhoria são bastante remotas. O código se parece com:

// use a < for an inclusive lower bound and exclusive upper bound
// use <= for an inclusive lower bound and inclusive upper bound
// alternatively, if the upper bound is inclusive and you can pre-calculate
//  upper-lower, simply add + 1 to upper-lower and use the < operator.
    if ((unsigned)(number-lower) <= (upper-lower))
        in_range(number);

Com um computador moderno e típico (ou seja, qualquer coisa que use dois complementos), a conversão para não assinado é realmente uma nop - apenas uma mudança na maneira como os mesmos bits são visualizados.

Observe que, em um caso típico, você pode pré-calcular upper-lowerfora de um loop (presumido), para que isso normalmente não contribua com um tempo significativo. Juntamente com a redução do número de instruções de ramificação, isso também (geralmente) melhora a previsão de ramificação. Nesse caso, a mesma ramificação é obtida, independentemente do número estar abaixo da extremidade inferior ou acima da extremidade superior do intervalo.

Quanto a como isso funciona, a idéia básica é bastante simples: um número negativo, quando visto como um número não assinado, será maior do que qualquer coisa que tenha começado como um número positivo.

Na prática, esse método converte numbere o intervalo para o ponto de origem e verifica se numberestá no intervalo [0, D], onde D = upper - lower. Se numberabaixo do limite inferior: negativo e se acima do limite superior: maior queD .

Jerry Coffin
fonte
8
@ TomásBadan: Os dois terão um ciclo em qualquer máquina razoável. O que é caro é o ramo.
26713 Oliver
3
Ramificações adicionais são feitas devido a curto-circuito? Se for esse o caso, lower <= x & x <= upper(em vez de lower <= x && x <= upper) resultaria em melhor desempenho também?
Markus Mayr
6
@ AK4749, jxh: Por mais legal que seja essa pepita, hesito em votar, porque infelizmente não há nada que sugira que isso seja mais rápido na prática (até que alguém faça uma comparação das informações de montagem e criação de perfil resultantes). Pelo que sabemos, compilador do OP pode tornar o código do OP com um único opcode ramo ...
Oliver Charlesworth
152
UAU!!! Isso resultou em uma melhoria de ordem de magnitude no meu aplicativo para esta linha de código específica. Ao pré-computar o canto superior e inferior, meu perfil passou de 25% do tempo dessa função para menos de 2%! Gargalo é agora operações de adição e subtração, mas eu acho que pode ser bom o suficiente agora :)
jjxtra
28
Ah, agora o @PsychoDad atualizou a pergunta, está claro por que isso é mais rápido. O código real tem um efeito colateral na comparação, e é por isso que o compilador não conseguiu otimizar o curto-circuito.
Oliver Charlesworth
17

É raro conseguir otimizações significativas para codificar em uma escala tão pequena. Grandes ganhos de desempenho advêm da observação e modificação do código de um nível superior. Você pode eliminar completamente a necessidade do teste de faixa ou apenas O (n) deles em vez de O (n ^ 2). Você pode reordenar os testes para que um lado da desigualdade esteja sempre implícito. Mesmo que o algoritmo seja ideal, é mais provável que ocorram ganhos quando você ver como esse código faz o teste de intervalo 10 milhões de vezes e encontrar uma maneira de agrupá-los e usar o SSE para fazer muitos testes em paralelo.

Ben Jackson
fonte
16
Apesar das votações negativas, mantenho minha resposta: O assembly gerado (veja o link pastebin em um comentário à resposta aceita) é bastante terrível para algo no loop interno de uma função de processamento de pixels. A resposta aceita é um truque interessante, mas seu efeito dramático está muito além do que é razoável esperar para eliminar uma fração de um ramo por iteração. Algum efeito secundário está dominando, e eu ainda espero que uma tentativa de otimizar todo o processo ao longo deste teste deixe os ganhos da comparação inteligente de alcance na poeira.
Ben Jackson
17

Depende de quantas vezes você deseja executar o teste com os mesmos dados.

Se você estiver executando o teste uma única vez, provavelmente não há uma maneira significativa de acelerar o algoritmo.

Se você estiver fazendo isso para um conjunto muito finito de valores, poderá criar uma tabela de pesquisa. A execução da indexação pode ser mais cara, mas se você pode ajustar a tabela inteira no cache, poderá remover todas as ramificações do código, o que deve acelerar as coisas.

Para seus dados, a tabela de pesquisa seria 128 ^ 3 = 2.097.152. Se você pode controlar uma das três variáveis ​​e considerar todas as instâncias em que start = Nao mesmo tempo, o tamanho do conjunto de trabalho cai para 128^2 = 16432bytes, que devem se encaixar bem nos caches mais modernos.

Você ainda teria que fazer referência ao código real para ver se uma tabela de pesquisa sem ramificação é suficientemente mais rápida que as comparações óbvias.

Andrew Prock
fonte
Então, você armazenaria algum tipo de pesquisa com um valor, início e fim e ele conteria um BOOL informando se estava no meio?
Jjxtra #
Corrigir. Seria uma tabela de pesquisa 3D: bool between[start][end][x]. Se você sabe como será o seu padrão de acesso (por exemplo, x está aumentando monotonicamente), é possível projetar a tabela para preservar a localidade, mesmo que a tabela inteira não caiba na memória.
Andrew Prock,
Vou ver se consigo tentar esse método e ver como ele funciona. Estou pensando em fazê-lo com um vetor de bit por linha, onde o bit será definido se o ponto estiver no círculo. Acha que será mais rápido que um byte ou int32 vs o mascaramento de bits?
jjxtra
2

Esta resposta é para relatar um teste feito com a resposta aceita. Realizei um teste de faixa fechada em um grande vetor de número inteiro aleatório classificado e, para minha surpresa, o método básico de (baixo <= num && num <= alto) é de fato mais rápido que a resposta aceita acima! O teste foi realizado no HP Pavilion g6 (AMD A6-3400APU com 6 GB de RAM. Aqui está o código principal usado para o teste:

int num = rand();  // num to compare in consecutive ranges.
chrono::time_point<chrono::system_clock> start, end;
auto start = chrono::system_clock::now();

int inBetween1{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (randVec[i - 1] <= num && num <= randVec[i])
        ++inBetween1;
}
auto end = chrono::system_clock::now();
chrono::duration<double> elapsed_s1 = end - start;

comparado com o seguinte, que é a resposta aceita acima:

int inBetween2{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (static_cast<unsigned>(num - randVec[i - 1]) <= (randVec[i] - randVec[i - 1]))
        ++inBetween2;
}

Preste atenção que o randVec é um vetor classificado. Para qualquer tamanho do MaxNum, o primeiro método supera o segundo na minha máquina!

rezeli
fonte
11
Meus dados não são classificados e meus testes estão na CPU do braço do iPhone. Seus resultados com dados e CPU diferentes podem ser diferentes.
Jjxtra # 1 de
classificado no meu teste foi apenas para garantir que o limite superior não seja menor que o limite inferior.
Rezeli
11
Os números classificados significam que a previsão de ramificações será muito confiável e acertará todas as ramificações, exceto algumas nos pontos de alternância. A vantagem do código sem ramificação é que ele se livra desses tipos de previsões errôneas em dados imprevisíveis.
Andreas Klebinger 7/02/19
0

Para qualquer verificação de faixa variável:

if (x >= minx && x <= maxx) ...

É mais rápido usar a operação de bit:

if ( ((x - minx) | (maxx - x)) >= 0) ...

Isso reduzirá dois ramos em um.

Se você se preocupa com o tipo seguro:

if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...

Você pode combinar mais verificação de faixa variável:

if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...

Isso reduzirá 4 ramificações em 1.

É 3,4 vezes mais rápido que o antigo no gcc:

insira a descrição da imagem aqui

skywind3000
fonte
-4

Não é possível apenas executar uma operação bit a bit no número inteiro?

Como deve estar entre 0 e 128, se o 8º bit estiver definido (2 ^ 7), será 128 ou mais. O caso extremo será um problema, pois você deseja uma comparação inclusiva.

água gelada
fonte
3
Ele quer saber se x <= end, onde end <= 128. Não x <= 128.
precisa
11
Esta declaração " Como deve estar entre 0 e 128, se o 8º bit estiver definido (2 ^ 7), é 128 ou mais " está errado. Considere 256.
Happy Green Kid Naps
11
Sim, aparentemente eu não pensei nisso o suficiente. Desculpa.
icedwater