Dispositivo compilador rápido. Parte 3


Continuamos a estudar o compilador Swift. Esta parte é dedicada ao Swift Intermediate Language.


Se você não viu os anteriores, recomendo que você siga o link e leia:



Silgen


O próximo passo é converter o AST digitado em SIL bruto. O Swift Intermediate Language (SIL) é uma representação intermediária criada especialmente para o Swift. Uma descrição de todas as instruções pode ser encontrada na documentação .


O SIL possui um formulário SSA. Atribuição única estática (SSA) - uma representação de código na qual cada variável recebe um valor apenas uma vez. É criado a partir do código regular, adicionando variáveis ​​adicionais. Por exemplo, usando um sufixo numérico que indica a versão de uma variável após cada atribuição.


Graças a este formulário, é mais fácil para o compilador otimizar o código. Abaixo está um exemplo de pseudo-código. Obviamente, a primeira linha é desnecessária:


a = 1 a = 2 b = a 

Mas isso é apenas para nós. Para ensinar o compilador a determinar isso, seria necessário escrever algoritmos não triviais. Mas com o SSA, isso é muito mais fácil. Agora, mesmo para um compilador simples, será óbvio que o valor da variável a1 não é usado e esta linha pode ser excluída:


 a1 = 1 a2 = 2 b1 = a2 

O SIL permite aplicar otimizações e verificações específicas ao código Swift que seriam difíceis ou impossíveis de concluir na fase AST.


Usando o gerador SIL


Para gerar SIL, use o sinalizador -emit-silgen :


 swiftc -emit-silgen main.swift 

O resultado do comando:


 sil_stage raw import Builtin import Swift import SwiftShims let x: Int // x sil_global hidden [let] @$S4main1xSivp : $Int // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$S4main1xSivp // id: %2 %3 = global_addr @$S4main1xSivp : $*Int // user: %8 %4 = metatype $@thin Int.Type // user: %7 %5 = integer_literal $Builtin.Int2048, 16 // user: %7 // function_ref Int.init(_builtinIntegerLiteral:) %6 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %7 %7 = apply %6(%5, %4) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %8 store %7 to [trivial] %3 : $*Int // id: %8 %9 = integer_literal $Builtin.Int32, 0 // user: %10 %10 = struct $Int32 (%9 : $Builtin.Int32) // user: %11 return %10 : $Int32 // id: %11 } // end sil function 'main' // Int.init(_builtinIntegerLiteral:) sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int 

O SIL, como o LLVM IR, pode ser emitido como código fonte. Você pode encontrar nele que, nesta fase, foi adicionada a importação dos módulos Swift Builtin, Swift e SwiftShims.


Apesar do código Swift poder ser gravado diretamente no escopo global, o SILGen gera a função principal - o ponto de entrada para o programa. Todo o código estava localizado dentro dele, exceto para declarar uma constante, pois é global e deve estar acessível em qualquer lugar.


A maioria das linhas tem uma estrutura semelhante. À esquerda, há um pseudo-registro no qual o resultado da instrução é salvo. Em seguida - a própria instrução e seus parâmetros, e no final - um comentário indicando o registro para o qual esse registro será usado.


Por exemplo, é criado nesta linha um literal inteiro do tipo Int2048 e um valor 16. Esse literal é armazenado no quinto registro e será usado para calcular o valor do sétimo:


 %5 = integer_literal $Builtin.Int2048, 16 // user: %7 

Uma declaração de função começa com a palavra-chave sil. A seguir está o nome com o prefixo @, convenção de chamada, parâmetros, tipo de retorno e código de função. Para o inicializador Int.init (_builtinIntegerLiteral :), é claro que não é especificado, pois essa função é de outro módulo e precisa ser declarada, mas não definida. Um cifrão indica o início de uma indicação de tipo:


 // Int.init(_builtinIntegerLiteral:) sil [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int 

A convenção de chamada indica como chamar uma função corretamente. Isso é necessário para gerar código de máquina. Uma descrição detalhada desses princípios está além do escopo deste artigo.


O nome dos inicializadores, bem como os nomes de estruturas, classes, métodos, protocolos, são distorcidos (nome incorreto). Isso resolve vários problemas ao mesmo tempo.


Primeiramente, permite usar os mesmos nomes em diferentes módulos e entidades aninhadas. Por exemplo, para o primeiro método fff , o nome S4main3AAAV3fffSiyF é usado e, para o segundo, S4main3BBBVVffffSiyF é usado :


 struct AAA { func fff() -> Int { return 8 } } struct BBB { func fff() -> Int { return 8 } } 

S significa Swift, 4 é o número de caracteres no nome do módulo e 3 está no nome da classe. No inicializador literal, Si indica o tipo padrão Swift.Int.


Em segundo lugar, nomes e tipos de argumentos de função são adicionados ao nome. Isso permite o uso de sobrecarga. Por exemplo, para o primeiro método, S4main3AAAV3fff3iiiS2i_tF é gerado e, para o segundo - S4main3AAAV3fff3dddSiSd_tF :


 struct AAA { func fff(iii internalName: Int) -> Int { return 8 } func fff(ddd internalName: Double) -> Int { return 8 } } 

Após os nomes dos parâmetros, o tipo do valor de retorno é indicado, seguido pelos tipos de parâmetros. No entanto, seus nomes internos não são indicados. Infelizmente, não há documentação sobre a manipulação de nomes no Swift, e sua implementação pode mudar a qualquer momento.


O nome da função é seguido por sua definição. Consiste em um ou mais blocos básicos. Um bloco básico é uma sequência de instruções com um ponto de entrada, um ponto de saída, que não contém instruções ou condições de ramificação para uma saída antecipada.


A função principal possui uma unidade base, que aceita todos os parâmetros passados ​​para a função e contém todo o seu código, pois não há ramificações nela:


 bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): 

Podemos assumir que cada escopo delimitado por chaves é uma unidade base separada. Suponha que o código contenha uma ramificação:


 // before if 2 > 5 { // true } else { // false } // after 

Nesse caso, pelo menos 4 blocos base serão gerados para:


  • código antes de ramificar,
  • casos em que a expressão é verdadeira
  • casos em que a expressão é falsa
  • código após ramificação.

cond_br - instrução para salto condicional. Se o valor do pseudo-registro% 14 for verdadeiro, a transição para o bloco bb1 será executada . Caso contrário, então no bb2 . br - salto incondicional que inicia a execução do bloco base especificado:


 // before cond_br %14, bb1, bb2 // id: %15 bb1: // true br bb3 // id: %21 bb2: // Preds: bb0 // false br bb3 // id: %27 bb3: // Preds: bb2 bb1 // after 

Código fonte:



Transformações garantidas SIL


A representação intermediária bruta obtida no último estágio é analisada quanto à correção e transformada em canônica: as funções marcadas como transparentes são inline (a chamada da função é substituída por seu corpo), os valores das expressões constantes são calculados, a função é verificada para verificar se as funções que retornam os valores faça isso em todas as ramificações de código e assim por diante.


Essas conversões são obrigatórias e são realizadas mesmo se a otimização do código estiver desativada.


Canon SIL Generation


Para gerar SIL canônico, o sinalizador -emit-sil é usado:


 swiftc -emit-sil main.swift 

O resultado do comando:


 sil_stage canonical import Builtin import Swift import SwiftShims let x: Int // x sil_global hidden [let] @$S4main1xSivp : $Int // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$S4main1xSivp // id: %2 %3 = global_addr @$S4main1xSivp : $*Int // user: %6 %4 = integer_literal $Builtin.Int64, 16 // user: %5 %5 = struct $Int (%4 : $Builtin.Int64) // user: %6 store %5 to %3 : $*Int // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // Int.init(_builtinIntegerLiteral:) sil public_external [transparent] [serialized] @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int { // %0 // user: %2 bb0(%0 : $Builtin.Int2048, %1 : $@thin Int.Type): %2 = builtin "s_to_s_checked_trunc_Int2048_Int64"(%0 : $Builtin.Int2048) : $(Builtin.Int64, Builtin.Int1) // user: %3 %3 = tuple_extract %2 : $(Builtin.Int64, Builtin.Int1), 0 // user: %4 %4 = struct $Int (%3 : $Builtin.Int64) // user: %5 return %4 : $Int // id: %5 } // end sil function '$SSi22_builtinIntegerLiteralSiBi2048__tcfC' 

Existem poucas mudanças em um exemplo tão simples. Para ver o verdadeiro trabalho do otimizador, você precisa complicar um pouco o código. Por exemplo, adicione adição:


 let x = 16 + 8 

Em seu SIL cru, você pode encontrar a adição desses literais:


 %13 = function_ref @$SSi1poiyS2i_SitFZ : $@convention(method) (Int, Int, @thin Int.Type) -> Int // user: %14 %14 = apply %13(%8, %12, %4) : $@convention(method) (Int, Int, @thin Int.Type) -> Int // user: %15 

Mas no canônico não está mais lá. Em vez disso, um valor constante de 24 é usado:


 %4 = integer_literal $Builtin.Int64, 24 // user: %5 

Código fonte:



Otimização de sil


Transformações específicas específicas do Swift são aplicadas se a otimização estiver ativada. Entre eles estão a especialização de genéricos (otimizando o código genérico para um tipo específico de parâmetro), a desvirtualização (substituindo chamadas dinâmicas por estáticas), inlining, a otimização do ARC e muito mais. Uma explicação dessas técnicas não se encaixa em um artigo já coberto de vegetação.


Código fonte:



Como o SIL é um recurso do Swift, desta vez não mostrei exemplos de implementação. Voltaremos ao compilador de parênteses na próxima parte, quando estaremos envolvidos na geração de IR do LLVM.

Source: https://habr.com/ru/post/pt438696/


All Articles