Swift under the hood: implementação genérica

O código genérico permite escrever funções e tipos flexíveis e reutilizáveis ​​que podem funcionar com qualquer tipo, sujeito aos requisitos que você definir. Você pode escrever um código que evite duplicação e expresse sua intenção de maneira clara e abstrata. - Documentos rápidos

Todo mundo que escrevia no Swift usava genéricos. Array , Dictionary , Set - as opções mais básicas para usar genéricos da biblioteca padrão. Como eles são representados por dentro? Vamos ver como esse recurso fundamental da linguagem é implementado pelos engenheiros da Apple.


Os parâmetros genéricos podem ser limitados por protocolos ou não, embora, basicamente, os genéricos sejam usados ​​em conjunto com protocolos que descrevem o que exatamente pode ser feito com parâmetros de método ou campos de tipo.


Para implementar genéricos, o Swift usa duas abordagens:


  1. Modo de tempo de execução - o código genérico é um invólucro (Boxe).
  2. Compiletime-way - o código genérico é convertido em um tipo específico de código para otimização (Especialização).

Boxe


Considere um método simples com um parâmetro genérico de protocolo ilimitado:


 func test<T>(value: T) -> T { let copy = value print(copy) return copy } 

O compilador rápido cria um único bloco de código que será chamado para trabalhar com qualquer <T> . Ou seja, independentemente de escrevermos test(value: 1) ou test(value: "Hello") , o mesmo código será chamado e informações adicionais sobre o tipo <T> contém todas as informações necessárias serão transferidas para o método .


Pouco pode ser feito com esses parâmetros de protocolo ilimitados, mas já para implementar esse método, você precisa saber como copiar um parâmetro, precisa saber o seu tamanho para alocar memória para ele em tempo de execução, precisa saber como destruí-lo quando o parâmetro sair do campo visibilidade. A Value Witness Table ( VWT ) é usada para armazenar essas informações. VWT é criado no estágio de compilação para todos os tipos e o compilador garante que, em tempo de execução, exista exatamente esse layout do objeto. Deixe-me lembrá-lo de que as estruturas no Swift são passadas por valor e as classes por referência; portanto, coisas diferentes serão feitas para let copy = value com T == MyClass e T == MyStruct .


Tabela de testemunhas de valor

Ou seja, chamar o método de test com a aprovação da estrutura declarada eventualmente se parecerá com isso:


 //  ,  metadata   let myStruct = MyStruct() test(value: myStruct, metadata: MyStruct.metadata) 

As coisas ficam um pouco mais complicadas quando o MyStruct uma estrutura genérica e assume o formato MyStruct<T> . Dependendo do <T> dentro do MyStruct , os metadados e o VWT serão diferentes para os tipos MyStruct<Int> e MyStruct<Bool> . Esses são dois tipos diferentes em tempo de execução. Mas a criação de metadados para todas as combinações possíveis de MyStruct e T extremamente ineficiente, então o Swift segue o caminho contrário e, nesses casos, constrói metadados em tempo de execução em movimento. O compilador cria um padrão de metadados para a estrutura genérica, que pode ser combinada com um tipo específico e, como resultado, receber informações completas do tipo em tempo de execução com o VWT correto.


 //   ,  metadata   func test<T>(value: MyStruct<T>, tMetadata: T.Type) { //       let myStructMetadata = get_generic_metadata(MyStruct.metadataPattern, tMetadata) ... } let myStruct = MyStruct<Int>() test(value: myStruct) //   test(value: myStruct, tMetadata: Int.metadata) //      

Quando combinamos informações, obtemos metadados com os quais podemos trabalhar (copiar, mover, destruir).


Ainda é um pouco mais complicado quando restrições de protocolo são adicionadas aos genéricos. Por exemplo, restringimos <T> protocolo Equatable . Que seja um método muito simples que compara os dois argumentos passados. O resultado é apenas um invólucro sobre o método de comparação.


 func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } 

Para que o programa funcione corretamente, você deve ter um ponteiro para o método de comparação static func ==(lhs:T, rhs:T) . Como conseguir isso? Obviamente, a transmissão VWT não VWT suficiente, não contém essas informações. Para resolver esse problema, existe uma Protocol Witness Table ou PWT . Esse VWT é semelhante ao VWT e é criado no estágio de compilação de protocolos e descreve esses protocolos.


 isEquals(first: 1, second: 2) //   //     isEquals(first: 1, // 1 second: 2, metadata: Int.metadata, // 2 intIsEquatable: Equatable.witnessTable) // 3 

  1. Dois argumentos passados
  2. Passe metadados para Int para poder copiar / mover / destruir objetos
  3. Passamos as informações que Int implementa Equatable .

Se a restrição exigisse a implementação de outro protocolo, por exemplo, T: Equatable & MyProtocol , as informações sobre o MyProtocol seriam adicionadas com o seguinte parâmetro:


 isEquals(..., intIsEquatable: Equatable.witnessTable, intIsMyProtocol: MyProtocol.witnessTable) 

O uso de wrappers para implementar genéricos permite implementar de forma flexível todos os recursos necessários, mas possui uma sobrecarga que pode ser otimizada.


Especialização genérica


Para eliminar a necessidade desnecessária de obter informações durante a execução do programa, foi utilizada a chamada abordagem de especialização genérica. Permite substituir um wrapper genérico por um tipo específico por uma implementação específica. Por exemplo, para duas chamadas para isEquals(first: 1, second: 2) e isEquals(first: "Hello", second: "world") , além da implementação principal do "wrapper", duas versões adicionais completamente diferentes do método para Int e para String .


Código fonte


Primeiro, crie um arquivo generic.swift e escreva uma pequena função genérica que consideraremos.


 func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } isEquals(first: 10, second: 11) 

Agora você precisa entender o que eventualmente se transforma em um compilador.
Isso pode ser visto claramente compilando nosso arquivo .swift no Swift Intermediate Language ou SIL .


Um pouco sobre o SIL e o processo de compilação


SIL é o resultado de um dos vários estágios da compilação rápida.


Pipeline do compilador

O código-fonte .swift é passado para o Lexer, que cria uma árvore de sintaxe abstrata ( AST ) do idioma, com base em que verificação de tipo e análise semântica do código são realizadas. O SilGen converte AST para SIL , chamado raw SIL , com base no qual o código é otimizado e um canonical SIL otimizado canonical SIL obtido, que é passado para o IRGen para conversão em IR - um formato especial que o LLVM entende, que será convertido em , . ` .o , . , . SIL`.


E novamente para os genéricos


Crie um arquivo SIL partir do nosso código-fonte.


 swiftc generic.swift -O -emit-sil -o generic-sil.s 

Temos um novo arquivo com a extensão *.s . Olhando para dentro, veremos um código muito menos legível que o original, mas ainda relativamente claro.


Sil bruto

Encontre a linha com o comentário // isEquals<A>(first:second:) . Este é o começo da descrição do nosso método. Ele termina com um comentário // end sil function '$s4main8isEquals5first6secondSbx_xtSQRzlF' . Seu nome pode ser um pouco diferente. Vamos analisar um pouco a descrição do método.


  • %0 e %1 na linha 21 são o first e o second parâmetros, respectivamente
  • Na linha 24, obtemos informações de tipo e passamos para %4
  • Na linha 25, obtemos um ponteiro para um método de comparação a partir de informações de tipo
  • na linha 26 Chamamos o método por ponteiro, passando os parâmetros e as informações de tipo
  • Na linha 27, damos o resultado.

Como resultado, vemos: para executar as ações necessárias na implementação do método genérico, precisamos obter informações da descrição do tipo <T> durante a execução do programa.


Prosseguimos diretamente para a especialização.


No arquivo SIL compilado, imediatamente após a declaração do método geral isEquals , isEquals a declaração do especialista para o tipo Int .



SIL especializado

Na linha 39, em vez de obter o método em tempo de execução a partir das informações de tipo, o método para comparar números inteiros "cmp_eq_Int64" chamado imediatamente.


Para que o método seja "especializado", a otimização deve estar ativada . Você também precisa saber que


O otimizador só pode executar especialização se a definição da declaração genérica estiver visível no Módulo atual ( Origem )

Ou seja, o método não pode ser especializado entre diferentes módulos Swift (por exemplo, o método genérico da biblioteca Cocoapods). Uma exceção é a biblioteca Swift padrão, na qual tipos básicos como Array , Set e Dictionary . Todos os genéricos da biblioteca base são especializados em tipos específicos.


Nota: Os atributos @inlinable e @usableFromInline foram implementados no Swift 4.2, que permite ao otimizador ver os corpos dos métodos de outros módulos e parece que há uma oportunidade de especializá-los, mas esse comportamento não foi testado por mim ( fonte )


Referências


  1. Descrição dos genéricos
  2. Otimização no Swift
  3. Apresentação mais detalhada e aprofundada sobre o tema.
  4. Artigo em inglês

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


All Articles