A convenção de chamada Rust que merecemos
(mcyoung.xyz)- A convenção de chamada atual
extern "Rust"do Rust depende do caminho da convenção de chamada C do LLVM e é conservadora no uso de registradores ao passar valores complexos, perdendo oportunidades de gerar código melhor - A ideia central é separar, com a flag por crate
-Zcallconv, o modo atuallegacye um novo modo orientado a registradores,fast, usando uma ABI mais agressiva em builds otimizadas - Mesmo sem adicionar uma nova convenção de chamada diretamente ao LLVM, é possível controlar a disposição dos argumentos com assinaturas fixas de função no LLVM e valores
poisonpara deixar argumentos de registrador não usados vazios sem custo - Tipos Rust como structs, enums, unions,
booleResultpodem ser passados de forma mais densa com base em tamanho efetivo sem padding, achatamento, bit packing e heurísticas de divisão entre pilha e registradores - Se o corpo da função, informações do borrow checker e dados de perfil forem refletidos na decisão da ABI, otimizações ainda mais fortes se tornam possíveis, mas a complexidade da geração de código de ABI no rustc e a falta de especialização em LLVM continuam sendo barreiras reais
As otimizações de convenção de chamada que o Rust perde hoje
- A convenção de chamada (calling convention) é parte da ABI que define como argumentos e valores de retorno são transmitidos, quais registradores são usados e como prólogo/epílogo e unwinding são tratados
- O Rust define sua própria convenção de chamada não especificada, mas na prática ela é reduzida para a convenção de chamada C embutida do LLVM e depende da geração de código de prólogo/epílogo do LLVM
- O rustc age de forma conservadora para tentar gerar assinaturas de função no LLVM como as que o Clang produziria
- Isso pode reduzir a chance de quebrar depuradores
- Também pode diminuir a chance de acionar bugs no LLVM por meio de caminhos de geração de ABI pouco usados pelo Clang
- Em sistemas baseados em ELF, o DWARF não fixa a ABI C do Linux, então, no escopo do texto, a possibilidade de depuração não é tratada como o problema principal
- Em um exemplo simples,
fn extract(arr: [i32; 3]) -> i32, o array de 12 bytes é passado por ponteiro em vez de registradores- Se for usado
extern "C", o mesmo[i32; 3]é passado empacotado emrdiersi - É um caso em que o caminho padrão do Rust é ainda mais conservador do que a ABI C do Linux
- Se for usado
-Zcallconv: separando legacy e fast
- A convenção de chamada atual de
extern "Rust"é mantida, mas a flag de compilação por crate-Zcallconvescolhe qual convenção usar-Zcallconv=legacy: modo atual-Zcallconv=fast: novo modo orientado a registradores-Opoderia até definir automaticamente-Zcallconv=fast
- A convenção
fastnão organiza os argumentos na ordem da ABI C, então pode parecer confusa para quem espera a ordem idiomática de registradores no x86 - Em alvos como WASM, onde não há conceito de registradores e spilling,
-Zcallconv=fasttalvez não seja suportado - Em builds de debug sem otimização,
fastpode gerar código pior, então sua ativação pode não ser apropriada - Ponteiros de função e blocos
extern "Rust" {}exigem restrições separadas- A flag vale no nível do crate, mas é difícil expressar em ponteiros de função qual versão de
extern "Rust"está sendo usada - Chamadas por ponteiro de função podem ser tratadas como caminhos lentos e raros, forçando
-Zcallconv=legacy - Se necessário, pode-se gerar um shim que converte a convenção de chamada
- Por causa do caminho que permite chamar símbolos sem mangling, símbolos
#[no_mangle]também podem acabar usando a convenção legacy
- A flag vale no nível do crate, mas é difícil expressar em ponteiros de função qual versão de
Controlando o LLVM de forma indireta
- Idealmente, seria possível especificar diretamente ao LLVM algo como “este argumento vai neste registrador, este valor de retorno vai naquele”, mas adicionar uma convenção de chamada ao LLVM exigiria bastante código em C++
- Em vez disso, é possível obter um efeito próximo de uma convenção própria com o seguinte processo
- Determinar, por target triple, o número máximo de valores que podem ser passados em registradores
- Decidir se o valor de retorno cabe em registradores de saída ou se deve ser retornado por referência com um argumento extra
ptrmarcado comsret - Rebaixar argumentos by-value muito grandes para by-reference
- Escolher quais argumentos irão para registradores de forma a maximizar a ocupação do espaço de registradores
- Colocar os demais argumentos na pilha
- Montar a assinatura da função em LLVM IR com argumentos não agregados como
i64,ptr,doublee<2 x i64> - No prólogo da função, decodificar as entradas de registrador em argumentos no nível Rust
- No bloco de saída da função, codificar o retorno no formato de saída necessário e então fazer
ret - Para funções não polimórficas, não inline e cujo endereço pode ser tomado, gerar um shim legacy para preservar a identidade do ponteiro de função
- O problema de decidir quais valores colocar em registradores é parecido com o problema da mochila (knapsack problem) e é NP-hard, então a implementação prática exige heurísticas
- Essas informações podem ser colocadas no
rmetapara evitar recálculo em vez de serem computadas tarde demais - Como o Rust já quebra ABI entre releases, a exigência de impedir o link de código gerado por compiladores Rust diferentes já está alinhada com a situação atual
Os limites do LLVM para passagem por registradores
- O LLVM tenta “explodir” ao máximo argumentos by-value agregados em registradores ao passá-los para funções
- No x86, os dados de entrada que o LLVM consegue passar em registradores são aproximadamente estes
- 6 inteiros
- 8 vetores SSE
- para retorno, metade disso: 3 inteiros e 4 vetores
- Em
aarch64-unknown-linux, tanto entrada quanto saída podem usar 8 inteiros e 8 vetores - É possível projetar todas as funções
-Zcallconv=fastno x86 para terem o mesmo número de argumentos passados por registrador- 6 argumentos para registradores inteiros
- 8 argumentos vetoriais de
xmm0atéxmm7 - ao passar ponteiros reais, o
i64correspondente viraptr - ao passar
double, ele substitui uma posição<2 x i64>
- Mesmo que a maioria das funções não passe 176 bytes, é possível evitar custo extra enviando
poisondo LLVM nos argumentos não usados- O LLVM pode tratar
poisoncomo o valor que for mais conveniente no momento - Se
poisonfor passado como argumento de registrador, isso pode ser tratado como “o valor que já estava naquele registrador”, sem necessidade de tocá-lo - No exemplo, o código que passa um ponteiro para
load_rcx()emrcxepoisonnos outros 13 registradores não gera instrução alguma após otimização
- O LLVM pode tratar
- Essa técnica dá controle quase total sobre a passagem de argumentos, mas a situação ideal de usar os mesmos registradores para entrada e saída varia por arquitetura
- ARM e RISC-V ficam mais próximos de uma estrutura em que entrada e saída usam os mesmos registradores
- x86 não é assim, mas ainda é possível reduzir movimentos desnecessários de registrador assumindo uma ordem diferente de alocação
Ajustando melhor os tipos Rust aos registradores
- Ao lidar com structs e unions Rust, assume-se que o rustc já tratou os tipos do usuário como aggregates e unions básicos, e então se decide que partes vão para registradores
- Para valores de retorno, o tamanho efetivo sem padding é mais importante do que o tamanho total da struct
[(u64, u32); 2]ocupa 32 bytes no total, mas 8 bytes são padding- Ao achatar para
(u64, u32, u64, u32)e ordenar por tamanho como(u64, u64, u32, u32), o total cai para 24 bytes - Isso cabe nos 3 registradores inteiros de retorno do x86
- O tamanho efetivo é definido pelo número de bits não
undef[(u64, u32); 2]tem 192 bitsbooltem 1 bitchartecnicamente tem 21 bits, mas por simplicidade é tratado como um alias deu32
- Structs com muitos
boolpodem retornar váriosboolcom bit packing em um único registrador - No lado dos argumentos isso é mais difícil, e algumas heurísticas possíveis são
- Rebaixar para by-reference argumentos cujo tamanho efetivo seja maior que todo o espaço de entrada by-register
- No x86, o espaço total de entrada é de 176 bytes, ou 1408 bits
- Transformar enums em um par de discriminant e union
Option<i32>pode ser visto internamente como(union { i32, () }, i1)Option<Option<i32>>pode ser visto como(union { i32, (), () }, i2)
- Como unions podem tocar bits não inicializados arbitrariamente, em geral elas são passadas como um array de
u8 - Uma union com apenas uma variant não vazia pode ser substituída por essa própria variant
- O argumento transformado é achatado em primitivas como ponteiro, inteiro, float e bool
- Campos maiores que um registrador de argumento pequeno, como
u128ef64, podem ser divididos - A lista de primitivas é ordenada por tamanho efetivo, e escolhe-se o maior prefixo que caiba nos registradores
- O restante vai para a pilha
- Se a parte que iria para a pilha for maior que um pequeno múltiplo do tamanho de ponteiro, ela pode ser rebaixada para pointer-on-the-stack para reduzir tráfego de memória
- Valores passados por registrador são alocados dos maiores para os menores, e
boolpode ser compactado até 64 por registrador
Exemplo de função Rust complexa e os limites atuais do rustc
- No exemplo de
do_thing, que recebeOption<usize>,&dyn Context,&str,[char; 6]e uma structOptions, após achatamento e ordenação todos os argumentos brutos do LLVM cabem em registradores - Os tipos LLVM dos argumentos brutos no exemplo ficam nesta forma
gprs: i64, ptr, ptr, ptr, i64, i32, i32xmm0: i32, i32, i32, i32xmm1: i32, i1, i1, i1, i1
- O prólogo da função extrai as primitivas e recompõe os valores no nível Rust
Option<usize>vira{ i64, i1 }- o trait object vira
{ ptr, ptr } &strvira{ ptr, i64 }[char; 6]vira[6 x i32]Optionsvira{ i32, i1, i1, i1 }
- Se as instruções que materializam os valores dos argumentos receberem metadados
!dbg, o gdb pode mostrar os valores dos argumentos com resultados melhores - Hoje o rustc passa 8 parâmetros do tamanho de ponteiro para o LLVM para essa mesma função e, como resultado, usa todos os 6 registradores inteiros e envia 2 valores pela pilha
Espaço para otimização em retornos e Result
- Esse desenho não cobre todas as otimizações possíveis de convenção de chamada
- Em alguns casos, registradores extras como os AVX do x86 também poderiam ser usados
- Também pode ser considerado passar uma struct dividida entre registradores e pilha
- O retorno de
Resulttem espaço próprio para otimização- Quando ele atravessa várias camadas de função via
?, pode haver muitos movimentos redundantes de registrador - Se o
Resultfor grande demais para caber em registradores, cada chamada com?na pilha de chamadas pode precisar carregar da memória e testar o bit de ok - Como alternativa, o erro pode ser mantido em um ponteiro de out-parameter, enquanto o payload da variant ok e o bit is-ok são retornados como
Option<T> - Os detalhes de
?quando envolve chamada deIntosão complicados, mas implementáveis
- Quando ele atravessa várias camadas de função via
ABI dependente de otimização
- Diferentemente de C, em
-Zcallconv=fasto Rust pode olhar o corpo da função ao montar a ABI vista pelo chamador - O crate pode anunciar, por função, a ABI exata do ponto de vista de passagem por registradores
- A otimização mais simples é eliminar da ABI argumentos que não são usados
- Se a função não usa certo parâmetro, nenhum registrador é gasto com ele
- Se um argumento
&Tnão for mantido, não virar raw pointer,Tfor pequeno eT: Freeze, o próprio valor apontado pode ser passado por valor em vez da referência - APIs como
HashMap::get()são candidatas- Se a key for um tipo como
i32, hoje é preciso fazer spill do inteiro para a pilha e passar seu ponteiro - Esse tráfego de memória pode ser evitado
- Se a key for um tipo como
- Uma ABI guiada por perfil é ainda mais agressiva
- Argumentos mais hot podem ter prioridade na ordem de alocação de registradores
- Mesmo ao receber uma struct grande por referência, 3 campos
i64hot podem ser carregados previamente pelo chamador e passados tanto por ponteiro quanto por registradores - O callee teria de fazer esses loads de qualquer forma, então não haveria custo adicional
- Um perfil por instrumentação pode até justificar a duplicação de funções que diferem apenas na ABI
Por que isso ainda não foi feito
- O Rust tem menos restrições de ABI que o C++, então consegue gerar código melhor, e a ideia se aproxima do que a Go register ABI já usa na prática
- O primeiro obstáculo é a complexidade da geração de código de ABI
- O LLVM quase não oferece knobs úteis de controle
- Mesmo dentro do rustc, essa não é uma área amigável
- Uma implementação errada pode prejudicar bastante a usabilidade
- Outro obstáculo é a falta de especialização
- Entre os contribuidores do rustc, são poucos os que entendem bem a semântica do LLVM e as características da geração de código a ponto de produzir bom código sem fazer o LLVM travar
- O tempo de compilação também pode ser um peso
- Quanto mais complexa a assinatura da função, mais código de prólogo/epílogo o LLVM precisa processar
- Ainda assim, como
-Zcallconvé pensado para uso apenas com otimização ativada, isso não é visto como desvantagem decisiva
- O código de ABI do Rust é uma área com baixo bus factor, e conhecimento de LLVM pode ser aplicado diretamente para ajudar o time do compilador Rust a gerar código mais otimizado
1 comentários
Comentários do Hacker News
Ao otimizar uma calling convention, o principal não é imaginar mentalmente qual formato parece melhor, e sim medir o desempenho
Código bom é código rápido, não código que parece rápido
Às vezes, o que o autor chama de código ruim acaba sendo o mais rápido por motivos nada intuitivos, e isso só dá para saber medindo em benchmarks grandes
Um dos motivos de uma calling convention que parece ruim funcionar bem é que ela economiza registradores de argumentos, deixando o alocador de registradores um pouco mais à vontade
Além disso, as CPUs de hoje são otimizadas para o fluxo de instruções gerado por compiladores C; então, se você gerar um código no estilo de compiladores C que fazem passagem pela pilha com uma frequência surpreendente, especialmente o MSVC, pode acabar acertando o ponto ideal da CPU
Como o inlining está tão eficiente, chamadas em caminhos quentes acabam virando limites raros, e mesmo que esse limite fique um pouco bagunçado, tudo bem se isso simplificar as outras partes
Isso não quer dizer que a mudança seja ruim, mas discutir apenas olhando para código que parece estranho, sem medições, é estranho
Eu trabalhei profissionalmente com otimização de calling convention no JavaScriptCore, e era surpreendentemente comum ver código de passagem pela pilha que parecia pior vencer em código real de grande porte
Mas também acho que resultados de medição de desempenho não deveriam ser o único critério
Na expressão de que as CPUs estão otimizadas “hoje em dia”, a palavra importante é “hoje”, porque as CPUs continuam mudando, então a calling convention precisa ser um projeto de longo prazo
Por isso, infelizmente, é melhor não se afastar demais da forma como o C++ faz isso. É provável que as otimizações dos processadores no futuro também mirem esse lado
Ao mesmo tempo, é bom considerar princípios gerais que não mudam facilmente, como economizar registradores de argumentos, para tornar a calling convention robusta e preparada para o futuro
O Rust parece ter ficado conservador demais nos últimos anos em relação ao orçamento de estranheza (https://steveklabnik.com/writing/the-language-strangeness-bu...), então é até um pouco curioso eu dizer isso. No fim, sem ser diferente, não dá para melhorar
Se a função já começa pegando o endereço dos parâmetros e passando para uma função desconhecida, no fim das contas isso vai ter de ir para a pilha de qualquer jeito
Seria interessante ver uma otimização de calling convention baseada no corpo da função. No caso de funções estáticas em C, parece seguro desde que o endereço não seja obtido
Um JIT já entra nessa questão com muita informação sobre a CPU real em execução antes mesmo de gerar uma única linha de assembly, então tem vantagem nisso
Em código puramente compilado de forma estática, não dá para saber em tempo de execução qual é o conjunto de recursos da arquitetura, então você acaba encontrando com frequência barreiras de inlining justamente no código que mais gostaria de otimizar
Hoje o Rust parece fraco nesse aspecto em plataformas pequenas, e a calling convention pode ajudar nisso em relação ao retorno de
ResultAinda assim, fico curioso se as vantagens empíricas da passagem pela pilha continuam valendo em CPUs ARMV8 ou RISC-V, que têm muitos registradores
É um rascunho razoável, mas falta a distinção entre caller-saved e callee-saved, e há o erro comum de alocar parte dos registradores de entrada para a saída
Também é otimista esperar que o depurador vá entender uma convenção de chamada diferente da de C. Não importa o que o DWARF consiga codificar, na prática há uma grande chance de isso falhar miseravelmente
Mudar a ABI conforme a configuração de otimização interage muito mal com a compilação separada
Reorganizar argumentos como se fosse um empacotamento denso até funcionaria, mas aumentaria bastante a complexidade do compilador, e não sei se isso vale mais a pena do que uma alocação simples da esquerda para a direita no primeiro encaixe possível. Também ficaria mais difícil para o desenvolvedor prever para onde cada argumento vai
A direção geral de usar convenções de chamada diferentes para funções cujo endereço escapa e para as que não escapa faz sentido. Também funciona bem separar prólogos que façam o casamento de impedância
Rust deveria estar disposto a ter uma convenção de chamada diferente da de C, mas não sei se isso precisa ser uma única convenção rígida usada por todas as funções. Colocar isso no sistema de tipos parece natural, e deixar o desenvolvedor controlar a convenção de chamada elimina uma das vantagens de desempenho do assembly
Do ponto de vista do chamador, de qualquer forma os registradores de saída precisam estar livres entre duas chamadas de função, e isso também é bem comum em convenções de chamada de system calls
Imagino que a intenção seja facilitar para o callee preparar os valores de saída sem mexer nos valores de entrada. Se for isso, até entendo querer colocar os registradores de saída no fim da ordem dos registradores de entrada para evitar sobreposição, mas não vejo muito motivo para proibir completamente qualquer sobreposição
Function AchamandoFunction B,Function CeFunction D, para reduzir overheadFico em dúvida sobre qual seria a semântica que preservaria esse tipo de otimização e ao mesmo tempo permitiria esse controle, ou se isso no fim não passa de uma ilusão
Na prática, assembly quase nunca é alvo da maioria das otimizações de compilador, então há uma desvantagem de desempenho. Muitas vezes ele também não recebe otimizações do tipo “analisar o comportamento, concluir que é totalmente redundante e eliminar tudo de uma vez”, e já não estamos mais nos anos 1990
Ainda assim, se for um caso em que esse tipo de otimização nem pode ser considerado, então eu diria que o único ponto em que assembly inline realmente perde com certeza é em otimização guiada por perfil. Isso porque o desenvolvedor da aplicação conhece perfeitamente o comportamento do código, e o desenvolvedor do compilador não
O overhead de chamada pode ser eliminado usando mais assembly até cobrir os limites quentes relevantes
bool, em especial, pode criar uma cadeia de dependênciaNo x64, para
bool, não parece haver jeito muito melhor do que primeiro colocá-los em registradores, fazer shift e aplicar OR no resultadoA abordagem simples cria uma cadeia de dependência de comprimento 64 e pode gerar uma penalidade de 64 ciclos, embora com cuidado talvez dê para reduzir isso para 6 ciclos e, realisticamente, algo em torno de 12 ciclos
Mas também existe a questão de onde viriam 64
bool. Não há tantos registradores assim, então no fim seria preciso reler da pilhaSe a ABI do Rust já empacota
booldentro de structs de forma tão densa assim, então isso de qualquer maneira já teria de ser feito, mas não sei ao certoE depois o chamador ainda precisa desempacotar tudo de novo
Parece mais fácil ensinar o compilador a derramar os valores para o espaço de resultado na pilha, e isso provavelmente também teria desempenho melhor
Nesse caso, fico em dúvida sobre o quanto colocar valores em registradores realmente ajuda
A convenção de chamada de C é meio ruim
É verdade que não dá para mudar a convenção de chamada de C, mas isso não a torna menos frustrante
Todos os registradores caller-saved disponíveis deveriam ser usados para argumentos e valores de retorno, mas na ABI SysV tradicional só se usa um registrador para retorno, às vezes dois
Se você retorna
struct Point3D { long x, y, z }, daria para colocarPoint3Demrax,rdi,rsi, mas em vez disso ele é derramado para a pilhaOutros sistemas têm outros truques. Se lembro bem, no SBCL uma função define o carry flag ao terminar quando retorna múltiplos valores. Por exemplo, talvez fosse bom usar o carry flag para indicar se um
Resultcontém um erroA convenção de chamada de C basicamente suporta o que C suporta, isto é, retornar um único argumento. Nem retorno de struct ela faz direito
Em C, isso fica mais para um “não era óbvio?”, e no lado de C++ vira algo como “é só fazer inline”
Por outro lado, spills para memória realmente acontecem. Por exemplo, o espaço generoso de registradores e as janelas do SPARC deixavam muitos registradores sem uso em funções simples, e quando o anel de registradores sofria spill isso levava a um uso grande de pilha que destruía o cache
No x86, mesmo com muitos
movpara rearranjar dados para “onde eles precisam estar”, no fim isso muitas vezes era mais rápidoQuando se olha só para o código do callee, dá vontade de dizer “esse argumento aqui e esse valor de retorno ali certamente seriam mais rápidos”, mas não dá para saber nada sobre o chamador
Não dá para garantir que a preparação dos argumentos vá passar direto, nem que o valor de retorno vá ser consumido de forma quente. Por exemplo, se
struct Point { x: i32, y: i32, z: i32 }for usado como argumento/retorno e o chamador estiver num loop fazendo algo comomystruct.deepinside.point[i] = func(mystruct.deepinside.point[i]), colocar e tirar isso de registradores pode virar overhead ou até impedir vetorizaçãoO callee não tem como saber disso, exceto quando o compilador consegue ver os dois lados e fazer inline
O fruto mais baixo na árvore em relação a chamadas parece ser remover a suposição, embutida em quase toda ABI de C, de que funções retornam um único valor primitivo. Para o resto, seria preciso muito benchmarking e muitas estatísticas de geração de código
Há outro detalhe frustrante em Rust que faz structs crescerem mais do que se gostaria
Pense em uma struct
Foocom 8 camposOptionque sãoNoneouSome(u8); em C, isso pode ser representado em 9 bytes no total, com 8boolde 1 bit e 8uint8_tEm Rust, isso vira 16 bytes, com 1 byte de discriminante e
uint8_trepetido 8 vezesA razão é que a struct precisa ser capaz de fornecer empréstimos de seus campos. Se você tem
&Foo, o compilador precisa ser capaz de criar&Foo::some_field, isto é,&Option, e esse&Optionprecisa ter a mesma forma que todos os outros&Optiondo programaPortanto, o
Optioninterno precisa ter o mesmo layout que outrosOptiondo programa: seu bit discriminante arredondado para um byte, mais ou8. A struct paga esse custo mesmo que&Foo::some_fieldnunca seja criadoFica pior se você pensar em
Optionde tipos maiores. Numa struct com 8 camposOption, cada discriminante é arredondado para 2 bytes, totalizando 32 bytes, e um quarto disso — quase metade se contar os bits não usados dos discriminantes — é desperdiçado como padding intermediário. O equivalente em C precisa de 18 bytesAo usar
Option, a struct em Rust pode ficar com 128 bytes, enquanto a struct em C pode ter 72 bytesClaro, é possível implementar a mesma representação de C colocando um
u8para discriminantes empacotados e 8MaybeUninit, e escrevendo manualmente funções que mapeiem&FooparaOption<&T>e&mut FooparaOption<&mut T>. Só não dá para passar por&Optionou&mut Optionhttps://play.rust-lang.org/?version=stable&mode=debug&editio...
Você está basicamente descrevendo um tipo definido pelo usuário que contém 8
Option, e, quando se começa a se preocupar com desempenho, é preciso tocar manualmente o tratamento interno dessesOptionO fato de Rust oferecer recursos convenientes que você pode escolher quando combinam com o objetivo não chega a ser uma desvantagem
O caso de uso descrito é relativamente raro e, se for um gargalo real de desempenho, gastar um pouco mais de tempo implementando isso em Rust não é um grande problema
Em uso geral, os benefícios do tipo
Option<_>são enormes, então é difícil considerar isso um “detalhe frustrante” de RustMudando um pouco de assunto: há um trecho dizendo que, se o endereço de uma função não polimórfica e não inline puder ser obtido como ponteiro de função, então se criaria um shim usando
-Zcallconv=legacye a implementação real faria tail call imediata; entendo a intenção de preservar a equivalência de ponteiros de funçãoMas, se o shim legado fizer tail call para uma função com calling convention de Rust, isso não impediria de corrigir a diferença no valor de retorno da convenção?
Em um tema um pouco diferente, fiquei curioso se hoje existe interoperação entre Go e Rust
Lembro de ter visto no passado um caso que conseguiu isso colocando Zig no meio, mas não consigo encontrar. Tenho código legado em Rust e gostaria de migrar aos poucos para Go
extern "C"com CGOO pessoal da busca de código do GitHub apresentou como faz isso na RustConf 2023(https://www.youtube.com/watch?v=KYdlqhb267c) e depois ouvi dizer que lugares como a 1Password também estão fazendo algo parecido
Não é divertido ficar transportando tipos através da fronteira de interoperabilidade com C, mas é possível e também permite reutilizar código
extern "C"e chamá-las de Go como se fossem CNo sentido contrário, não sei muito bem
O código gerenciado precisa poder possuir memória que será liberada ou movida, e o código não gerenciado precisa raciocinar sobre quando a memória será liberada ou movida
Coisas como
cgopermitem misturar chamadas FFI da parte gerenciada do Go para memória não gerenciada, mas isso tem um custoEsse problema sempre aparece em implementações nas quais as linguagens que se chamam mutuamente não compartilham o coletor de lixo
Misturar código gerenciado/não gerenciado é uma ideia antiga, mas ainda é um tema de pesquisa ativo
A menos que o runtime embutido tenha sido projetado para isso, chamar código gerenciado a partir de código não gerenciado quase sempre é uma má ideia, e normalmente entra uma camada de serialização no meio
Se esse for o seu trabalho principal, talvez você ache ruim, mas eu cansei de voltar ao código a cada poucas semanas e não lembrar mais como tudo funcionava
Passei um closure Rust com estado como callback para código Go e o injetei numa função da biblioteca padrão de Go, incluindo até o desenrolar de panic dentro do closure Rust
https://github.com/Voultapher/sort-research-rs/commit/df6c91...
Fiquei um tempão no Inspecionar elemento tentando descobrir como o título da seção foi deixado inclinado, mas pelas ferramentas do Safari eu não estava conseguindo ver. Como isso foi feito, afinal?
.post-title:transform: skewY(-2deg) translate(-1rem, -0.4rem);element()(https://developer.mozilla.org/en-US/docs/Web/CSS/element), mas na verdade era uma cópia do corpo do texto reduzida a um tamanho minúsculoh1, h2, h3, h4, h5, h6são aplicadostransform:skewY(-2deg) translate(-1rem,0rem);,transform-origin:top;,font-style:italic;,text-decoration-line:underline;,text-decoration-color:goldenrod;,text-underline-offset:4%;,text-decoration-thickness:.25exEm contraste, existe o texto de 2019 “How Swift Achieved Dynamic Linking Where Rust Couldn't”
https://faultlore.com/blah/swift-abi/
É uma pena que Rust ainda não tenha uma calling convention para semântica em nível de Rust, mas ao mesmo tempo esse texto mostra que a quantidade de trabalho necessária para chegar lá é enorme
A Apple tinha uma motivação muito forte para tornar Swift uma linguagem de sistema prática da qual aplicações pudessem depender, mas Rust não tem esse tipo de patrocínio
Discussão no HN: https://news.ycombinator.com/item?id=21488415
Seria bom se Rust tivesse mais opções de suporte para esse compromisso, e isso não precisaria ficar limitado a coisas como https://github.com/rust-lang/rfcs/pull/3470
Se o compilador atual de Rust fizer inline agressivamente e depois otimizar, fico em dúvida se isso realmente vale o esforço
Se a função chamada for pequena, ela será colocada inline; se for grande, então bastante tempo será gasto dentro dela, então o overhead da chamada será pequeno
dyn Trait, não podem ser colocadas inline, então esse tipo de mudança ajudaSe der para baratear as chamadas, talvez não seja necessário fazer inline de forma tão agressiva, o que também pode ajudar no tamanho do código e no tempo de compilação
Funções complexas demais para inline provavelmente farão alguns acessos à memória, e esses acessos têm boa chance de serem o gargalo
Passar dados pela stack aperta ainda mais esse gargalo, porque aumenta a pressão de cache e a quantidade de loads/stores
Se Rust conseguir passar argumentos de forma ideal em uma fração significativa das chamadas de função, isso não só evitaria alguns ciclos de acesso à L1, como também poderia fazer a CPU atingir mais rápido o gargalo de memória essencial
Talvez o ganho seja de alguns poucos por cento, mas agora estou bebendo vinho e não vou fazer as contas
Alguém pode explicar qual é a mnemônica “Diana’s silk dress cost $89” que aparece nos materiais de referência sobre x86?