Por que o LLVM aloca uma variável redundante?

9

Aqui está um arquivo C simples com uma definição de enumeração e uma mainfunção:

enum days {MON, TUE, WED, THU};

int main() {
    enum days d;
    d = WED;
    return 0;
}

Transpila para o seguinte LLVM IR:

define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 2, i32* %2, align 4
  ret i32 0
}

%2é evidentemente a dvariável, que recebe 2 atribuídos a ela. A que %1corresponde se zero é retornado diretamente?

macleginn
fonte
11
Quais sinalizadores você usou para produzir esse RI?
arrowd 6/01
@arrowd, instalei a última suíte LLVM estável e executeiclang-9 -S -emit-llvm simple.c
macleginn
11
Eu acho que tem algo a ver com a inicialização antes main( godbolt.org/z/kEtS-s ). O link mostra como o assembly é mapeado para a fonte
Pradeep Kumar
2
@PradeepKumar: De fato, se você alterar o nome da função para algo diferente main, a variável extra misteriosa desaparecerá. Curiosamente, ele também desaparece se você omitir returncompletamente a declaração (o que é legal mainem C e equivalente a return 0;).
Nate Eldredge
11
@ macleginn: Não tenho tanta certeza. Se você declara maincomo int main(int argc, char **argv)argce argvcopia na pilha, mas a variável zero misteriosa ainda está lá além delas.
Nate Eldredge

Respostas:

3

Esse %1registro foi gerado pelo clang para manipular várias instruções de retorno em uma função . Imagine que você tivesse uma função para calcular o fatorial de um número inteiro. Em vez de escrever assim

int factorial(int n){
    int result;
    if(n < 2)
      result = 1;
    else{
      result = n * factorial(n-1);
    }
    return result;
}

Você provavelmente faria isso

int factorial(int n){
    if(n < 2)
      return 1;
    return n * factorial(n-1);
}

Por quê? Porque Clang irá inserir a resultvariável que contém o valor de retorno para você. Yay. Esse é o propósito exato disso %1. Veja no ir uma versão ligeiramente modificada do seu código.

Código modificado,

enum days {MON, TUE, WED, THU};

int main() {
    enum days d;
    d = WED;
    if(d) return 1;
    return 0;
}

IR,

define dso_local i32 @main() #0 !dbg !15 {
    %1 = alloca i32, align 4
    %2 = alloca i32, align 4
    store i32 0, i32* %1, align 4
    store i32 2, i32* %2, align 4, !dbg !22
    %3 = load i32, i32* %2, align 4, !dbg !23
    %4 = icmp ne i32 %3, 0, !dbg !23
    br i1 %4, label %5, label %6, !dbg !25

 5:                                                ; preds = %0
   store i32 1, i32* %1, align 4, !dbg !26
   br label %7, !dbg !26

 6:                                                ; preds = %0
  store i32 0, i32* %1, align 4, !dbg !27
  br label %7, !dbg !27

 7:                                                ; preds = %6, %5
  %8 = load i32, i32* %1, align 4, !dbg !28
  ret i32 %8, !dbg !28
}

Agora você vê isso %1se tornando útil, hein? Como os outros apontaram, para funções com apenas uma instrução de retorno, essa variável provavelmente será removida por um dos passes otimizados do llvm.

solte no topo
fonte
1

Por que isso importa - qual é o problema real?

Eu acho que a resposta mais profunda que você está procurando pode ser: a arquitetura do LLVM é baseada em frontends bastante simples e em muitas passagens. Os frontends precisam gerar o código correto, mas não precisa ser um bom código. Eles podem fazer a coisa mais simples que funciona.

Nesse caso, o Clang gera algumas instruções que acabam não sendo usadas para nada. Isso geralmente não é um problema, porque alguma parte do LLVM se livra de instruções supérfluas. Clang confia que isso aconteça. Clang não precisa evitar emitir código morto; sua implementação pode se concentrar na correção, simplicidade, testabilidade etc.

arnt
fonte
1

Porque o Clang é feito com análise de sintaxe, mas o LLVM nem começou com a otimização.

O front-end do Clang gerou IR (Representação Intermediária) e não código de máquina. Essas variáveis ​​são SSAs (atribuições estáticas únicas); eles ainda não foram vinculados a registros e, na verdade, após a otimização, nunca serão porque são redundantes.

Esse código é uma representação literal da fonte. É o que chama a atenção do LLVM para otimização. Basicamente, o LLVM começa com isso e otimiza a partir daí. De fato, para a versão 10 e x86_64, o llc -O2 acabará gerando:

main: # @main
  xor eax, eax
  ret
Olsonista
fonte
Eu entendo o processo nesse nível. Eu queria saber por que esse RI foi gerado para começar.
macleginn
Você pode estar pensando em um compilador como uma única passagem. Há um pipeline de passes começando com o front-end do Clang, que gera IR. Ele nem sequer gerou esse IR textual que alguém solicitou com clang -emit-llvm -S file.cpp O Clang realmente gerou uma versão de código de bits serializável binária do IR. O LLVM é estruturado como várias passagens, cada uma capturando e otimizando o IR. O primeiro passe LLVM obtém IR de Clang. É necessário IR porque você pode substituir o Clang pelo Fortran FE para oferecer suporte a outro idioma com o mesmo otimizador + gerador de código.
Olsonist