Encontrar o maior de cinco inteiros pequenos o mais rápido possível

9

Eu uso uma variação de um filtro mediano de 5 cruzamentos nos dados de imagem em um pequeno sistema incorporado, ou seja,

    x
  x x x
    x

O algoritmo é realmente simples: leia 5 valores inteiros não assinados, obtenha os 2 mais altos, faça alguns cálculos sobre esses valores e escreva novamente o resultado inteiro não assinado.

O interessante é que os 5 valores de entrada inteira estão todos no intervalo de 0 a 20. O valor inteiro calculado também está na faixa de 0 a 20!

Através da criação de perfil, eu descobri que obter os dois maiores números é o gargalo, por isso quero acelerar essa parte. Qual é a maneira mais rápida de realizar essa seleção?

O algoritmo atual usa uma máscara de 32 bits com 1 na posição fornecida pelos 5 números e uma função CLZ suportada por HW.
Devo dizer que a CPU é proprietária, não disponível fora da minha empresa. Meu compilador é o GCC, mas foi feito sob medida para esta CPU.

Tentei descobrir se posso usar uma tabela de pesquisa, mas não consegui gerar uma chave que possa usar.

Eu tenho combinações para a entrada, mas a ordem não é importante, ou seja, é a mesma que .215[5,0,0,0,5][5,5,0,0,0]

Acontece que a função hash abaixo produz um hash perfeito sem colisões!

def hash(x):
    h = 0
    for i in x:
        h = 33*h+i
    return h

Mas o hash é enorme e simplesmente não há memória suficiente para usá-lo.

Existe um algoritmo melhor que eu possa usar? É possível resolver meu problema usando uma tabela de pesquisa e gerando uma chave?

Fredrik Pihl
fonte
11
Qual algoritmo você usa atualmente? Sete comparações inteiras são suficientes, isso é muito lento? Você hashjá realiza mais operações. As chamadas subseqüentes ao método estão relacionadas, por exemplo, a central xpercorre a matriz linha por linha?
Raphael
O filtro é convolvido através da imagem linha por linha. Ou seja, obtenha os 5 valores e faça os cálculos, depois mova tudo um passo para a direita e repita. O hash foi apenas um exemplo. Comparei várias soluções de janelas deslizantes para minimizar a leitura de dados, mas tudo se resume a encontrar os 2 valores mais altos.
Fredrik Pihl
3
Provavelmente, seu algoritmo, se implementado corretamente, seria limitado pelo acesso à memória e não pelo cálculo. Usar uma tabela de hash aumentaria apenas a quantidade de acessos à memória e atrasaria as coisas. Poste seu código atual para que possamos ver como ele pode ser aprimorado - acredito que apenas a micro-otimização é possível. O máximo que consigo pensar é: talvez possamos tirar vantagem do fato de que 2 valores são comuns entre as janelas vizinhas?
JKFF
@jkff Dependendo da matriz, tamanhos do cache e função de mapeamento (cache), todo valor pode ter que ser carregado apenas uma vez; a maioria das operações deve ser executada em registradores ou cache L1. Pipelining é outra questão, no entanto.
Raphael
11
A propósito, você já faz isso em paralelo? Isso parece particularmente adequado para paralelização de vetores ou SIMD (por exemplo, em uma GPU). Essa rota ajudaria muito mais do que economizar alguns por cento por célula.
Raphael

Respostas:

11

Na minha outra resposta , sugiro que saltos condicionais possam ser o principal impedimento à eficiência. Como consequência, as redes de classificação vêm à mente: elas são independentes de dados, ou seja, a mesma sequência de comparações é executada independentemente da entrada, com apenas os swaps condicionais.

Obviamente, classificar pode ser muito trabalhoso; só precisamos dos dois maiores números. Para nossa sorte, as redes de seleção também foram estudadas. Knuth diz-nos que encontrar os dois números menores fora do five² pode ser feito com L 2 ( 5 ) = 6 comparações [1, 5.3.4 ex 19] (e, no máximo, como muitos swaps).você^2(5)=6

A rede que ele fornece nas soluções (reescrita para matrizes baseadas em zero) é

[0 0:4][1 1:4][0 0:3][1 1:3][0 0:2][1 1:2]

que implementa - após ajustar a direção das comparações - no pseudocódigo como

def selMax2(a : int[])
  a.swap(0,4) if a[0] < a[4]
  a.swap(1,4) if a[1] < a[4]
  a.swap(0,3) if a[0] < a[3]
  a.swap(1,3) if a[1] < a[3]
  a.swap(0,2) if a[0] < a[2]
  a.swap(1,2) if a[1] < a[2]
  return (a[0], a[1])
end

Agora, implementações ingênuas ainda têm saltos condicionais (através do código de troca). Dependendo da sua máquina, você pode contorná-las com instruções condicionais. x86 parece ser o habitual eu no poço da lama; O ARM parece mais promissor, pois aparentemente a maioria das operações é condicional em si mesma. Se eu entendi as instruções corretamente, a primeira troca se traduz nisso, supondo que nossos valores de matriz tenham sido carregados nos registros R0por meio de R4:

CMP     R0,R4
MOVLT   R5 = R0
MOVLT   R0 = R4
MOVLT   R4 = R6

Sim, sim, é claro que você pode usar a troca de XOR com EOR .

Eu só espero que seu processador tenha isso ou algo semelhante. Obviamente, se você criar a coisa para esse fim, talvez consiga conectar a rede lá?

Este é provavelmente (provavelmente?) O melhor que você pode fazer no reino clássico, ou seja, sem fazer uso do domínio limitado e executar mágicas intrincadas em palavras.


  1. Classificação e pesquisa de Donald E. Knuth; A arte da programação de computadores vol. 3 (2ª ed., 1998)
  2. W^2(5)=7
Rafael
fonte
Estou aceitando isso. Recebi muitas idéias novas que preciso avaliar antes de prosseguir. Referir-se a Knuth sempre funciona para mim :-) Obrigado pelo seu esforço e tempo!
Fredrik Pihl
@FredrikPihl Legal, por favor, deixe-nos saber como fica no final!
Raphael
Eu vou! Lendo o capítulo 5.3.3 agora. Ame o início da-lo com referências a Lewis Carroll e o torneio de tênis :-)
Fredrik Pihl
2
Dependendo do conjunto de instruções, usar 2 * max (a, b) = a + b + abs (ab) junto com a rede de seleção pode ser útil; poderia ser menos dispendioso do que saltos condicionais imprevisíveis (mesmo sem uma movimentação intrínseca ou condicional para abs: gcc, pelo menos para x86, gera uma sequência sem jato que não parece depender de x86). Ter uma sequência sem jato também é útil quando combinado com SIMD ou GPU.
AProgrammer 27/02
11
Observe que as redes de seleção (como redes de classificação) são passíveis de operações paralelas; especificamente na rede de seleção especificada, as comparações 1: 4 e 0: 3 podem ser executadas em paralelo (se o processador, o compilador etc. suportar com eficiência) e as comparações 1: 3 e 0: 2 também podem ser realizadas em paralelo.
Bruce Lilly
4

Apenas para que fique sobre a mesa, aqui está um algoritmo direto:

// Sort x1, x2
if x1 < x2
  M1 = x2
  m1 = x1
else
  M1 = x1
  m1 = x2
end

// Sort x3, x4
if x3 < x4
  M2 = x4
  m2 = x3
else
  M2 = x3
  m2 = x4
end

// Pick largest two
if M1 > M2
  M3 = M1
  if m1 > M2
    m3 = m1
  else
    m3 = M2
  end
else
  M3 = M2
  if m2 > M1
    m3 = m2
  else
    m3 = M1
  end
end

// Insert x4
if x4 > M3
  m3 = M3
  M3 = x4
else if x4 > m3
  m3 = x4
end

Por uma implementação inteligente de if ... else, pode-se livrar de alguns saltos incondicionais que uma tradução direta teria.

Isso é feio, mas leva apenas

  • cinco ou seis comparações (ou seja, saltos condicionais),
  • nove a dez tarefas (com 11 variáveis, todas em registros) e
  • sem acesso adicional à memória.

W2(5)

Não se pode esperar que seja rápido em máquinas com tubulação; dado o alto percentual de saltos condicionais, a maior parte do tempo provavelmente seria gasta em estol.

Observe que uma variante mais simples - classifique x1e x2, em seguida, insira os outros valores posteriormente - faz quatro a sete comparações e apenas cinco a seis atribuições. Como eu espero que os saltos sejam de custo mais alto aqui, fiquei com esse.


  1. Classificação e pesquisa de Donald E. Knuth; A arte da programação de computadores vol. 3 (2ª ed., 1998)
Rafael
fonte
Eu me pergunto o que um compilador otimizador pode fazer com isso.
Raphael
Vou implementar isso e compará-lo com a atual solução baseada em CLZ. Obrigado pelo seu tempo!
Fredrik Pihl 25/02
11
@FredrikPihl Qual foi o resultado dos seus benchmarks?
Raphael
11
A abordagem baseada em SWAP supera o CLZ! No celular agora. Pode publicar mais dados outra vez, no celular agora
Fredrik Pihl
@FredrikPihl Cool! Estou feliz que a boa e velha abordagem da teoria possa (ainda) ser útil. :)
Raphael
4

Esse pode ser um ótimo aplicativo e caso de teste para o projeto Souper . O Souper é um super otimizador - uma ferramenta que utiliza uma curta sequência de código como entrada e tenta otimizá-lo o máximo possível (tenta encontrar uma sequência equivalente de código que será mais rápida).

Souper é um software livre. Você pode tentar executar o Souper no seu snippet de código para ver se ele pode melhorar.

Veja também o concurso de John Regehr para escrever código rápido para classificar 16 valores de 4 bits ; é possível que algumas das técnicas sejam úteis.

DW
fonte
Eu estaria interessado no que isso pode fazer nos programas que o OP está tentando.
Raphael
3

213

T[T[T[441*a+21*b+c]*21+d]*21+e]

214

212

212

Yuval Filmus
fonte