Mudança da classe de complexidade através da otimização do compilador?

8

Estou procurando um exemplo em que um algoritmo aparentemente está alterando sua classe de complexidade devido a estratégias de otimização de compilador e / ou processador.

Lorenz Lo Sauer
fonte
2
Definitivamente, isso pode acontecer com a complexidade do espaço, com algo como remover a recursão da cauda.
Eliot Ball
4
Simples: um loop S vazio (n) pode optimizado para longe O (1): ver esta pós SO: stackoverflow.com/questions/10300253/...
Doc Brown
É mais fácil encontrar exemplos disso em Haskell, embora não seja realmente otimização - apenas a semântica preguiçosos da língua o que significa que, potencialmente, grandes pedaços de código para funções que foram chamados não será avaliada porque os resultados nunca são usadas. É ainda bastante comum em Haskell definir funções recursivas sem limites, retornando listas infinitas. Desde que você use apenas um pedaço finito da lista, apenas uma quantidade finita da recursão será avaliada (curiosamente, possivelmente não recursivamente) e apenas essa parte finita da lista será computada.
31313 Steve1314
1
@ Steve314: Creio que houve um exemplo disso no Computer Language Benchmark Game, em que a implementação do Haskell foi 10 vezes mais rápida que a implementação do C devido ao simples fato de que os resultados do benchmark nunca foram impressos e, portanto, todo o programa Haskell essencialmente compilado atéint main(void) { exit(0); };
Jörg W Mittag
@ Jörg - não me surpreenderia, mas acho que os desenvolvedores de Haskell estavam trapaceando. Você pode forçar algo a ser avaliado avidamente no Haskell, se necessário, e por incrível que pareça, ele esteja lá principalmente para otimização - a avaliação rigorosa / ansiosa geralmente é muito mais rápida do que preguiçosa, porque evita as despesas gerais para adiar a avaliação. Um bom compilador Haskell captura muito disso usando "análise de rigidez", mas há momentos em que você precisa forçar o problema.
31313 Steve1314

Respostas:

4

Vamos usar um programa simples que imprima o quadrado de um número digitado na linha de comando.

#include <stdio.h>

int main(int argc, char **argv) {
    int num = atoi(argv[1]);
    printf("%d\n",num);
    int i = 0;
    int total = 0;
    for(i = 0; i < num; i++) {
        total += num;
    }
    printf("%d\n",total);
    return 0;
}

Como você pode ver, este é um cálculo de O (n), repetindo repetidamente.

Ao compilar isso, gcc -Sobtém-se um segmento que é:

LBB1_1:
        movl    -36(%rbp), %eax
        movl    -28(%rbp), %ecx
        addl    %ecx, %eax
        movl    %eax, -36(%rbp)
        movl    -32(%rbp), %eax
        addl    $1, %eax
        movl    %eax, -32(%rbp)
LBB1_2:
        movl    -32(%rbp), %eax
        movl    -28(%rbp), %ecx
        cmpl    %ecx, %eax
        jl      LBB1_1

Neste você pode ver a adição sendo feita, uma comparação e um retorno ao loop.

Fazendo a compilação com gcc -S -O3para obter otimizações do segmento entre as chamadas para printf:

        callq   _printf
        testl   %ebx, %ebx
        jg      LBB1_2
        xorl    %ebx, %ebx
        jmp     LBB1_3
LBB1_2:
        imull   %ebx, %ebx
LBB1_3:
        movl    %ebx, %esi
        leaq    L_.str(%rip), %rdi
        xorb    %al, %al
        callq   _printf

Agora, pode-se ver que ele não tem loop e, além disso, não adiciona. Em vez disso, há uma chamada na imullqual multiplica o número por si só.

O compilador reconheceu um loop e o operador matemático interno e o substituiu pelo cálculo apropriado.


Observe que isso incluiu uma chamada atoipara obter o número. Quando o número já existe no código, o complier pré-calcula o valor em vez de fazer chamadas reais, como demonstrado em uma comparação entre o desempenho do sqrt em C # e C em que sqrt(2)(uma constante) foi somada em um loop 1.000.000 de vezes.

Comunidade
fonte
7

A otimização de chamada de cauda pode reduzir a complexidade do espaço. Por exemplo, sem o TCO, essa implementação recursiva de um whileloop tem uma complexidade de espaço no pior caso Ο(#iterations), enquanto que com o TCO tem uma complexidade de espaço no pior caso Ο(1):

// This is Scala, but it works the same way in every other language.
def loop(cond: => Boolean)(body: => Unit): Unit = if (cond) { body; loop(cond)(body) }

var i = 0
loop { i < 3 } { i += 1; println(i) }
// 1
// 2
// 3

// E.g. ECMAScript:
function loop(cond, body) {
    if (cond()) { body(); loop(cond, body); };
};

var i = 0;
loop(function { return i < 3; }, function { i++; print(i); });

Isso nem precisa de TCO geral, apenas um caso especial muito estreito, a eliminação da recursão direta da cauda.

O que seria muito interessante, porém, é que a otimização do compilador não apenas altera a classe de complexidade, mas na verdade altera completamente o algoritmo.

O Glorioso Glasgow Haskell Compiler às vezes faz isso, mas isso não é realmente o que eu estou falando, isso é mais como fazer batota. O GHC possui uma linguagem de correspondência de padrões simples que permite ao desenvolvedor da biblioteca detectar alguns padrões de código simples e substituí-los por códigos diferentes. Ea implementação GHC da biblioteca padrão Haskell não conter algumas dessas anotações, para que usos específicos de funções específicas que são conhecidos por ser ineficiente são reescritas em versões mais eficientes.

No entanto, essas traduções são escritas por seres humanos e para casos específicos, é por isso que considero trapacear.

Um supercompilador pode ser capaz de alterar o algoritmo sem entrada humana, mas no AFAIK nenhum supercompilador em nível de produção foi criado.

Jörg W Mittag
fonte
Obrigado pelo ótimo exemplo e por mencionar o GHC. Mais uma pergunta: E a execução fora de ordem. Existe algum exemplo conhecido em que esse tipo de otimização levou a uma alteração na classe de complexidade de um algoritmo?
Lorenz Lo Sauer
0

Um compilador que está ciente de que a linguagem está usando big-num fazendo redução de força (substituindo multiplicações pelo índice de um loop por uma adição) alteraria a complexidade dessa multiplicação de O (n log n) na melhor das hipóteses para O (n) .

AProgrammer
fonte