Preguiça
Não é uma "otimização de compilador", mas é algo garantido pela especificação da linguagem, para que você possa sempre contar com isso. Essencialmente, isso significa que o trabalho não é realizado até que você "faça algo" com o resultado. (A menos que você faça uma de várias coisas para deliberadamente desativar a preguiça.)
Obviamente, esse é um tópico inteiro e, portanto, o SO já tem muitas perguntas e respostas.
Na minha experiência limitada, tornar seu código muito preguiçoso ou muito rigoroso tem penalidades de desempenho muito maiores (no tempo e no espaço) do que qualquer outra coisa sobre a qual estou prestes a falar ...
Análise de rigidez
Preguiça é evitar o trabalho, a menos que seja necessário. Se o compilador puder determinar que um determinado resultado será "sempre" necessário, não será necessário armazenar o cálculo e executá-lo posteriormente; apenas executará diretamente, porque é mais eficiente. Isso é chamado de "análise de rigidez".
O problema, obviamente, é que o compilador nem sempre pode detectar quando algo pode ser feito estrito. Às vezes, você precisa dar pequenas dicas ao compilador. (Não conheço nenhuma maneira fácil de determinar se a análise de rigidez fez o que você pensa que tem, além de analisar a saída do Core.)
Inlining
Se você chamar uma função e o compilador puder dizer para qual função você está chamando, ele poderá tentar "incorporar" essa função - ou seja, substituir a chamada de função por uma cópia da própria função. A sobrecarga de uma chamada de função geralmente é muito pequena, mas o inlining geralmente permite que outras otimizações ocorram, o que não teria acontecido de outra forma, portanto o inlining pode ser uma grande vitória.
As funções são incorporadas apenas se forem "suficientemente pequenas" (ou se você adicionar um pragma especificamente solicitando inlining). Além disso, as funções só podem ser incorporadas se o compilador puder dizer qual função você está chamando. Há duas maneiras principais que o compilador pode não conseguir dizer:
Se a função que você está chamando for passada de outro lugar. Por exemplo, quando ofilter
função é compilada, você não pode incorporar o predicado de filtro, porque é um argumento fornecido pelo usuário.
Se a função que você está chamando é um método de classe e o compilador não sabe que tipo está envolvido. Por exemplo, quando a sum
função é compilada, o compilador não pode incorporar a +
função, porque sum
funciona com vários tipos de números diferentes, cada um com um+
função .
No último caso, você pode usar o {-# SPECIALIZE #-}
pragma para gerar versões de uma função codificada para um tipo específico. Por exemplo, {-# SPECIALIZE sum :: [Int] -> Int #-}
compilaria uma versão dosum
código embutido para o Int
tipo, o que significa que +
pode ser incorporado nesta versão.
Note, no entanto, que nossa nova sum
função especial só será chamada quando o compilador puder dizer que estamos trabalhando Int
. Caso contrário, o original, polimórficosum
é chamado. Novamente, a sobrecarga real da chamada de função é bastante pequena. São as otimizações adicionais que o inlining pode permitir que são benéficas.
Eliminação de subexpressão comum
Se um determinado bloco de código calcular o mesmo valor duas vezes, o compilador poderá substituí-lo por uma única instância da mesma computação. Por exemplo, se você fizer
(sum xs + 1) / (sum xs + 2)
o compilador pode otimizar isso para
let s = sum xs in (s+1)/(s+2)
Você pode esperar que o compilador sempre faça isso. No entanto, aparentemente, em algumas situações, isso pode resultar em pior desempenho, nem melhor, portanto o GHC nem sempre faz isso. Francamente, eu realmente não entendo os detalhes por trás deste. Mas o ponto principal é que, se essa transformação é importante para você, não é difícil fazer isso manualmente. (E se não é importante, por que você está se preocupando com isso?)
Expressões de caso
Considere o seguinte:
foo (0:_ ) = "zero"
foo (1:_ ) = "one"
foo (_:xs) = foo xs
foo ( []) = "end"
Todas as três primeiras equações verificam se a lista está vazia (entre outras coisas). Mas verificar a mesma coisa três vezes é um desperdício. Felizmente, é muito fácil para o compilador otimizar isso em várias expressões de caso aninhadas. Nesse caso, algo como
foo xs =
case xs of
y:ys ->
case y of
0 -> "zero"
1 -> "one"
_ -> foo ys
[] -> "end"
Isso é bem menos intuitivo, mas mais eficiente. Como o compilador pode facilmente fazer essa transformação, você não precisa se preocupar com isso. Basta escrever sua correspondência de padrões da maneira mais intuitiva possível; o compilador é muito bom em reordenar e reorganizar isso para torná-lo o mais rápido possível.
Fusão
O idioma padrão Haskell para o processamento de listas é encadear funções que levam uma lista e produzem uma nova lista. O exemplo canônico sendo
map g . map f
Infelizmente, embora a preguiça garanta pular o trabalho desnecessário, todas as alocações e desalocações para o desempenho intermediário da lista diminuem. "Fusão" ou "desmatamento" é onde o compilador tenta eliminar essas etapas intermediárias.
O problema é que a maioria dessas funções é recursiva. Sem a recursão, seria um exercício elementar inline alinhar todas as funções em um grande bloco de código, executar o simplificador sobre ele e produzir um código realmente ótimo sem listas intermediárias. Mas por causa da recursão, isso não vai funcionar.
Você pode usar {-# RULE #-}
pragmas para corrigir um pouco disso. Por exemplo,
{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}
Agora, toda vez que o GHC vê o map
pedido map
, ele o esmaga em uma única passagem pela lista, eliminando a lista intermediária.
O problema é que isso funciona apenas para map
seguido de map
. Existem muitas outras possibilidades - map
seguidas por filter
, filter
seguidas por map
etc. Em vez de codificar manualmente uma solução para cada uma delas, a chamada "fusão de fluxo" foi inventada. Esse é um truque mais complicado, que não descreverei aqui.
O mais longo e mais curto é: Estes são todos os truques especiais de otimização escritos pelo programador . O próprio GHC não sabe nada sobre fusão; está tudo nas bibliotecas de listas e outras bibliotecas de contêineres. Portanto, quais otimizações acontecem depende de como as bibliotecas de contêiner são gravadas (ou, mais realista, quais bibliotecas você escolhe usar).
Por exemplo, se você trabalha com matrizes Haskell '98, não espere qualquer tipo de fusão. Mas entendo que a vector
biblioteca possui amplos recursos de fusão. É tudo sobre as bibliotecas; o compilador apenas fornece o RULES
pragma. (A propósito, o que é extremamente poderoso. Como autor de uma biblioteca, você pode usá-lo para reescrever o código do cliente!)
Meta:
Concordo com as pessoas que dizem "codifique primeiro, perfil segundo, otimize terceiro".
Também concordo com as pessoas que dizem "é útil ter um modelo mental para quanto custa uma determinada decisão de projeto".
Equilíbrio em todas as coisas, e tudo o que ...