Rust: Rastreamento de problema para `asm` (montagem embutida)

Criado em 9 nov. 2015  ·  111Comentários  ·  Fonte: rust-lang/rust

Este problema acompanha a estabilização da montagem embutida. O recurso atual não passou pelo processo de RFC e provavelmente precisará fazer isso antes da estabilização.

A-inline-assembly B-unstable C-tracking-issue T-lang requires-nightly

Comentários muito úteis

Gostaria de salientar que a sintaxe asm inline do LLVM é diferente daquela usada pelo clang / gcc. As diferenças incluem:

  • O LLVM usa $0 vez de %0 .
  • LLVM não suporta operandos asm nomeados %[name] .
  • O LLVM suporta diferentes tipos de restrição de registro: por exemplo "{eax}" vez de "a" em x86.
  • LLVM suporta restrições de registro explícitas ( "{r11}" ). Em C, você deve usar as variáveis ​​registrar asm para vincular um valor a um registro ( register asm("r11") int x ).
  • As restrições LLVM "m" e "=m" são basicamente quebradas. O Clang traduz isso em restrições de memória indiretas "*m" e "=*m" e passa o endereço da variável para o LLVM em vez da própria variável.
  • etc ...

O Clang converterá asm em linha do formato gcc para o formato LLVM antes de passá-lo para o LLVM. Ele também executa alguma validação das restrições: por exemplo, garante que "i" operandos são constantes de tempo de compilação,


À luz disso, acho que devemos implementar a mesma tradução e validação que o clang faz e dar suporte à sintaxe de asm inline do gcc adequada, em vez da estranha LLVM.

Todos 111 comentários

Haverá alguma dificuldade em garantir a compatibilidade com versões anteriores do assembly embutido no código estável?

@ main-- tem um ótimo comentário em https://github.com/rust-lang/rfcs/pull/1471#issuecomment -173982852 que estou reproduzindo aqui para a posteridade:

Com todos os bugs abertos e instabilidades em torno do asm! () (Há muitos ), eu realmente não acho que ele esteja pronto para a estabilização - embora eu adorasse ter o asm inline estável no Rust.

Devemos também discutir se o asm! () De hoje é realmente a melhor solução ou se algo na linha da RFC # 129 ou mesmo D seria melhor. Um ponto importante a considerar aqui é que asm () não suporta o mesmo conjunto de restrições que gcc. Portanto, podemos:

  • Atenha-se ao comportamento do LLVM e escreva documentos para isso (porque não consegui encontrar nenhum). Bom porque evita complexidade na ferrugem. Ruim porque confunde os programadores vindos de C / C ++ e porque algumas restrições podem ser difíceis de emular no código Rust.
  • Emular gcc e apenas criar um link para seus documentos : Legal porque muitos programadores já sabem disso e há muitos exemplos que podem ser copiados e colados com pequenas modificações. Ruim porque é uma extensão não trivial do compilador.
  • Faça outra coisa (como D faz): muito trabalho que pode ou não compensar. Se feito da maneira certa, isso pode ser muito superior ao estilo gcc em termos de ergonomia, embora possa se integrar melhor com a linguagem e o compilador do que apenas um blob opaco (muito acenando à mão aqui, pois não estou familiarizado o suficiente com componentes internos do compilador para avaliar isso) .

Finalmente, outra coisa a se considerar é o # 1201, que em seu design atual (eu acho) depende muito do conjunto embutido - ou conjunto embutido feito corretamente, para esse assunto.

Pessoalmente, acho que seria melhor fazer o que a Microsoft fez no MSVC x64: definir um conjunto (quase) abrangente de funções intrínsecas, para cada instrução asm, e fazer "asm inline" exclusivamente por meio dessas intrínsecas. Caso contrário, é muito difícil otimizar o código em torno do conjunto embutido, o que é irônico, já que muitos usos do conjunto embutido pretendem ser otimizações de desempenho.

Uma vantagem da abordagem com base intrínseca é que ela não precisa ser uma coisa tudo ou nada. Você pode definir os intrínsecos mais necessários primeiro e construir o conjunto de forma incremental. Por exemplo, para criptografia, tendo _addcarry_u64 , _addcarry_u32 . Observe que o trabalho para fazer os instrínsecos parece já ter sido feito completamente: https://github.com/huonw/llvmint.

Além disso, seria uma boa ideia adicionar os intrínsecos, mesmo que fosse decidido, em última instância, oferecer suporte ao conjunto embutido, pois eles são muito mais convenientes de usar (com base em minha experiência de usá-los em C e C ++), portanto, começando com os intrínsecos e vendo o quão longe chegamos parece uma coisa com risco zero de estarmos errados.

Os intrínsecos são bons, mas asm! pode ser usado para mais do que apenas inserir instruções.
Por exemplo, veja como estou gerando notas ELF em minha caixa probe .
https://github.com/cuviper/rust-libprobe/blob/master/src/platform/systemtap.rs

Espero que esse tipo de hackeagem seja raro, mas acho que ainda é uma coisa útil de se apoiar.

@briansmith

O conjunto embutido também é útil para código que deseja fazer sua própria alocação de registro / pilha (por exemplo, funções simples).

@briansmith sim, essas são algumas razões excelentes para usar intrínsecos sempre que possível. Mas é bom ter a montagem em linha como a escotilha de escape final.

@briansmith Note que asm!() é _uma espécie de_ um superconjunto de intrínsecos já que você pode construir o último usando o anterior. (O argumento comum contra este raciocínio é que o compilador poderia teoricamente otimizar _ através_ intrínsecos, por exemplo, retirá-los dos loops, executar CSE neles, etc. do que o compilador de qualquer maneira.) Consulte também https://github.com/rust-lang/rust/issues/29722#issuecomment -207628164 e https://github.com/rust-lang/rust/issues/29722# issuecomment -207823543 para casos em que asm em linha funcionam, mas o intrínseco não.

Por outro lado, os intrínsecos dependem criticamente de um "compilador suficientemente inteligente" para atingir _pelo menos_ o desempenho que se obteria com uma implementação de conjunto feita à mão. Meu conhecimento sobre isso está desatualizado, mas a menos que tenha havido um progresso significativo, as implementações baseadas em intrínsecos ainda são mensuravelmente inferiores em muitos - senão na maioria - dos casos. É claro que eles são muito mais convenientes de usar, mas eu diria que os programadores realmente não se importam muito com _isso_ quando estão dispostos a descer ao mundo das instruções específicas da CPU.

Agora, outra consideração interessante é que intrínsecos podem ser combinados com código de fallback em arquiteturas onde eles não são suportados. Isso oferece o melhor dos dois mundos: Seu código ainda é portátil - ele pode apenas empregar algumas operações aceleradas de hardware onde o hardware as suporta. É claro que isso só vale a pena para instruções muito comuns ou se o aplicativo tiver uma arquitetura de destino óbvia. Agora, a razão pela qual estou mencionando isso é que, embora alguém possa argumentar que isso pode ser potencialmente _injeável_ com intrínsecos _compilador-fornecidos_ (já que você provavelmente se preocuparia se realmente obteria as versões aceleradas mais a complexidade do compilador nunca é bom) I diria que é uma história diferente se os intrínsecos são fornecidos por uma _library_ (e apenas implementados usando conjunto embutido). Na verdade, este é o quadro geral que eu prefiro, embora possa me ver usando mais intrínsecos do que conjunto embutido.

(Eu considero os intrínsecos da RFC # 1199 um tanto ortogonais a esta discussão, pois existem principalmente para fazer o SIMD funcionar.)

@briansmith

Caso contrário, é muito difícil otimizar o código em torno do conjunto embutido, o que é irônico, já que muitos usos do conjunto embutido pretendem ser otimizações de desempenho.

Não tenho certeza do que você quer dizer aqui. É verdade que o compilador não pode quebrar o conjunto em suas operações individuais para fazer redução de força ou otimizações de olho mágico nele. Mas no modelo GCC, pelo menos, o compilador pode alocar os registradores que usa, copiá-lo quando ele replica os caminhos do código, excluí-lo se nunca for usado e assim por diante. Se o conjunto não for volátil, o GCC terá informações suficientes para tratá-lo como qualquer outra operação opaca como, digamos, fsin . Toda a motivação para o design estranho é fazer do conjunto embutido algo com que o otimizador possa mexer.

Mas eu não usei muito, especialmente não recentemente. E não tenho experiência com a versão do recurso do LLVM. Então, estou me perguntando o que mudou ou o que não entendi todo esse tempo.

Discutimos esse problema na semana de trabalho recente, pois a pesquisa de @japaric do ecossistema no_std tem a macro asm! como um dos recursos mais comumente usados. Infelizmente, não vimos um caminho fácil para estabilizar esse recurso, mas eu queria fazer algumas anotações para garantir que não esquecêssemos tudo isso.

  • Primeiro, atualmente não temos uma grande especificação da sintaxe aceita na macro asm! . Agora normalmente acaba sendo "olhe para LLVM" que diz "olhe para clang" que diz "olhe para gcc" que não tem bons documentos. No final, isso normalmente termina em "vá ler o exemplo de outra pessoa e adapte-o" ou "leia o código-fonte do LLVM". Para a estabilização, o mínimo é que precisamos ter uma especificação da sintaxe e documentação.

  • No momento, pelo que sabemos, não há garantia de estabilidade do LLVM. A macro asm! é uma ligação direta para o que o LLVM faz agora. Isso significa que ainda podemos atualizar o LLVM gratuitamente quando quisermos? O LLVM garante que nunca quebrará essa sintaxe? Uma maneira de aliviar essa preocupação seria ter nossa própria camada que compila com a sintaxe do LLVM. Dessa forma, podemos mudar o LLVM sempre que quisermos e se a implementação do assembly embutido no LLVM mudar, podemos apenas atualizar nossa tradução para a sintaxe do LLVM. Se asm! deve se tornar estável, basicamente precisamos de algum mecanismo para garantir a estabilidade em Rust.

  • No momento, existem alguns bugs relacionados à montagem embutida. A tag A-inline-assembly é um bom ponto de partida, e atualmente está repleta de ICEs, segfaults em LLVM, etc. No geral, esse recurso, conforme implementado hoje, não parece corresponder às garantias de qualidade que outros esperam de um estábulo recurso em Rust.

  • A estabilização da montagem embutida pode dificultar muito a implementação de um back-end alternativo. Por exemplo, back-ends como miri ou cranelift podem levar muito tempo para atingir a paridade de recursos com o back-end LLVM, dependendo da implementação. Isso pode significar que há uma fatia menor do que pode ser feito aqui, mas é algo importante a se ter em mente ao considerar a estabilização da montagem em linha.


Apesar dos problemas listados acima, queríamos ter certeza de que, pelo menos, sairíamos com alguma capacidade de avançar com esse problema! Para esse fim, fizemos um brainstorming de algumas estratégias de como podemos empurrar a montagem embutida para a estabilização. O principal caminho a seguir seria investigar o que o clang faz. Presumivelmente, clang e C têm sintaxe de assembly embutida efetivamente estável e pode ser provável que possamos apenas espelhar tudo o que o clang faz (especialmente wrt LLVM). Seria ótimo entender em mais detalhes como o clang implementa a montagem embutida. O clang tem sua própria camada de tradução? Ele valida algum parâmetro de entrada? (etc)

Outra possibilidade de seguir em frente é ver se há um montador que podemos simplesmente tirar da prateleira de outro lugar que já está estável. Algumas idéias aqui foram nasm ou o montador plan9. O uso do assembler do LLVM apresenta os mesmos problemas sobre garantias de estabilidade que a instrução de montagem embutida no IR. (é uma possibilidade, mas precisamos de uma garantia de estabilidade antes de usá-lo)

Gostaria de salientar que a sintaxe asm inline do LLVM é diferente daquela usada pelo clang / gcc. As diferenças incluem:

  • O LLVM usa $0 vez de %0 .
  • LLVM não suporta operandos asm nomeados %[name] .
  • O LLVM suporta diferentes tipos de restrição de registro: por exemplo "{eax}" vez de "a" em x86.
  • LLVM suporta restrições de registro explícitas ( "{r11}" ). Em C, você deve usar as variáveis ​​registrar asm para vincular um valor a um registro ( register asm("r11") int x ).
  • As restrições LLVM "m" e "=m" são basicamente quebradas. O Clang traduz isso em restrições de memória indiretas "*m" e "=*m" e passa o endereço da variável para o LLVM em vez da própria variável.
  • etc ...

O Clang converterá asm em linha do formato gcc para o formato LLVM antes de passá-lo para o LLVM. Ele também executa alguma validação das restrições: por exemplo, garante que "i" operandos são constantes de tempo de compilação,


À luz disso, acho que devemos implementar a mesma tradução e validação que o clang faz e dar suporte à sintaxe de asm inline do gcc adequada, em vez da estranha LLVM.

Há um excelente vídeo sobre resumos com D, MSVC, gcc, LLVM e Rust com slides online

Como alguém que adoraria poder usar ASM inline no Rust estável e com mais experiência do que gostaria de tentar acessar algumas das APIs MC do LLVM do Rust, alguns pensamentos:

  • O ASM embutido é basicamente uma cópia e colagem de um trecho de código no arquivo .s de saída para montagem, após alguma substituição de string. Ele também possui anexos de registros de entrada e saída, bem como registros substituídos. É improvável que esta estrutura básica realmente mude no LLVM (embora alguns dos detalhes possam variar um pouco), e eu suspeito que esta seja uma representação bastante independente da estrutura.

  • Construir uma tradução de uma especificação voltada para Rust para um formato IR voltado para LLVM não é difícil. E pode ser aconselhável - a sintaxe enferrujada {} para formatação não interfere com a linguagem assembly, ao contrário da notação $ LLVM e dos GCCs % .

  • O LLVM faz um trabalho surpreendentemente ruim na prática de realmente identificar quais registros são destruídos, particularmente em instruções não geradas pelo LLVM. Isso significa que é praticamente necessário que o usuário especifique manualmente quais registros serão eliminados.

  • Tentar analisar a montagem sozinho provavelmente será um pesadelo. A API LLVM-C não expõe a lógica MCAsmParser, e essas classes são muito chatas para trabalhar com o bindgen (eu fiz isso).

  • Para portabilidade para outros back-ends, contanto que você mantenha o assembly embutido principalmente no nível de "copiar e colar esta string com um pouco de alocação de registro e substituição de string", isso não deve inibir tanto os back-ends. Eliminar a constante de inteiro e as restrições de memória e manter apenas as restrições do banco de registradores não deve representar nenhum problema.

Estou brincando um pouco para ver o que pode ser feito com macros procedurais. Eu escrevi um que converte o assembly embutido do estilo GCC para o estilo ferrugem https://github.com/parched/gcc-asm-rs. Também comecei a trabalhar em um que usa uma DSL em que o usuário não precisa entender as restrições e todas são tratadas automaticamente.

Portanto, cheguei à conclusão de que acho que a ferrugem deve apenas estabilizar os blocos de construção básicos, então a comunidade pode iterar fora da árvore com macros para chegar às melhores soluções. Basicamente, basta estabilizar o estilo llvm que temos agora com apenas as restrições "r" e "i" e talvez "m", e sem interferências. Outras restrições e interferências podem ser estabilizadas posteriormente com seus próprios itens do tipo mini rfc.

Pessoalmente, estou começando a sentir que estabilizar esse recurso é o tipo de tarefa gigantesca que nunca será realizada a menos que alguém contrate um especialista em tempo integral para fazer isso por um ano inteiro. Quero acreditar que a sugestão de @parched de estabilizar asm! fragmentada tornará isso tratável. Espero que alguém pegue e corra com ele. Mas se não for, então precisamos parar de tentar alcançar a solução satisfatória que nunca chegará e chegar à solução insatisfatória que irá: estabilizar asm! como está, verrugas, ICEs, bugs e tudo , com avisos brilhantes e ousados ​​nos documentos que anunciam o travamento e a impossibilidade de transporte, e com a intenção de depreciar algum dia se uma implementação satisfatória descer milagrosamente, enviada por Deus, em seu anfitrião celestial. IOW, devemos fazer exatamente o que fizemos por macro_rules! (e, claro, assim como para macro_rules! , podemos ter um breve período de auxílio de banda frenético e à prova de futuro com vazamentos). Estou triste com as ramificações para back-ends alternativos, mas é vergonhoso para uma linguagem de sistemas relegar a montagem embutida a tal limbo, e não podemos deixar a possibilidade hipotética de vários back-ends continuar a obstruir a existência de um back-end realmente utilizável. Eu imploro, prove que estou errado!

é vergonhoso para uma linguagem de sistemas relegar a montagem inline a tal limbo

Como ponto de dados, estou trabalhando em uma caixa agora que depende de gcc com o único propósito de emitir algum asm com ferrugem estável: https://github.com/main--/unwind- rs / blob / 266e0f26b6423f4a2b8a8c72442b319b5c33b658 / src / unwind_helper.c


Embora certamente tenha suas vantagens, estou um pouco desconfiado da abordagem "estabilizar os blocos de construção e deixar o resto para proc-macros". Essencialmente, terceiriza o design, o RFC e o processo de implementação para quem deseja fazer o trabalho, potencialmente ninguém. Claro que ter garantias de estabilidade / qualidade mais fracas é o ponto principal (a desvantagem é que ter algo imperfeito já é muito melhor do que não ter nada), eu entendo isso.

Pelo menos os blocos de construção devem ser bem projetados - e na minha opinião, "expr" : foo : bar : baz definitivamente não é. Não me lembro de alguma vez ter acertado o pedido na primeira tentativa, sempre tenho que pesquisar. "Categorias mágicas separadas por dois pontos onde você especifica strings constantes com caracteres mágicos que acabam fazendo coisas mágicas com os nomes de variáveis ​​que você também misturou de alguma forma" é simplesmente ruim.

Uma ideia, ...

Hoje, já existe um projeto, denominado dynasm, que pode ajudá-lo a gerar código assembly com um plugin usado para pré-processar o assembly com um tipo de código x64.

Este projeto não responde ao problema de montagem inline, mas certamente pode ajudar, se rustc fornecer uma maneira de mapear variáveis ​​para registradores e aceitar inserir conjunto de bytes no código, tal projeto também poderia ser usado para preencher configurar esse conjunto de bytes.

Dessa forma, a única parte de padronização necessária do ponto de vista rustc, é a capacidade de injetar qualquer sequência de bytes no código gerado e de impor alocações de registro específicas. Isso remove todas as opções de sabores de idiomas específicos.

Mesmo sem o dynasm, isso também pode ser usado como uma forma de fazer macros para as instruções cpuid / rtdsc, que seriam apenas traduzidas na sequência bruta de bytes.

Acho que a próxima pergunta pode ser se queremos adicionar propriedades / restrições adicionais às sequências de bytes.

[EDITAR: Não acho que nada do que disse neste comentário esteja correto.]

Se quisermos continuar a usar o assembler integrado do LLVM (presumo que isso seja mais rápido do que gerar um assembler externo), então estabilização significa estabilizar exatamente quais expressões de assembly inline do LLVM e suporte de assembler integrado - e compensar as alterações, caso ocorra.

Se estivermos dispostos a gerar um montador externo, podemos usar qualquer sintaxe que quisermos, mas estaremos renunciando às vantagens do montador integrado e ficaremos expostos às mudanças em qualquer montador externo que estivermos chamando.

Eu acho que seria estranho se estabilizar no formato do LLVM quando nem mesmo o Clang faz isso. Presumivelmente, ele usa o suporte do LLVM internamente, mas apresenta uma interface mais parecida com o GCC.

Eu estou 100% correto em dizer "Rust suporta exatamente o que o Clang suporta" e encerrar o dia, especialmente porque a postura do AFAIK Clang é "Clang suporta exatamente o que o GCC suporta". Se alguma vez tivermos uma especificação Rust real, podemos suavizar a linguagem para "o assembly embutido é definido pela implementação". Precedência e padronização de fato são ferramentas poderosas. Se pudermos redirecionar o próprio código do Clang para traduzir a sintaxe GCC para LLVM, tanto melhor. As preocupações com o back-end alternativo não desaparecem, mas teoricamente um front-end do Rust para o GCC não seria muito incomodado. Menos para nós projetarmos, menos para pedalarmos sem parar, menos para ensinarmos, menos para mantermos.

Se estabilizarmos algo definido em termos do que o clang suporta, devemos chamá-lo de clang_asm! . O nome asm! deve ser reservado para algo que foi projetado por meio de um processo RFC completo, como outros recursos importantes do Rust. #garagem de bicicletas

Existem algumas coisas que eu gostaria de ver na montagem em linha do Rust:

  • O padrão de modelo com substituições é feio. Estou sempre saltando para frente e para trás entre o texto da montagem e a lista de restrições. A brevidade incentiva as pessoas a usar parâmetros posicionais, o que torna a legibilidade pior. Nomes simbólicos geralmente significam que você tem o mesmo nome repetido três vezes: no modelo, nomeando o operando, e na expressão sendo associada ao operando. Os slides mencionados no comentário de Alex mostram que D e MSVC permitem simplesmente fazer referência a variáveis ​​no código, o que parece muito mais agradável.

  • As restrições são difíceis de entender e (principalmente) redundantes com o código de montagem. Se Rust tivesse um montador integrado com um modelo suficientemente detalhado das instruções, ele poderia inferir as restrições nos operandos, removendo uma fonte de erro e confusão. Se o programador precisa de uma codificação específica da instrução, então ele precisa fornecer uma restrição explícita, mas isso geralmente não é necessário.

Norman Ramsey e Mary Fernández escreveram alguns artigos sobre o New Jersey Machine Code Toolkit , quando eles têm ideias excelentes para descrever pares de linguagem assembly / máquina de uma forma compacta. Eles lidam com codificações de instrução iA-32 (era do Pentium Pro); não se limita de forma alguma a ISAs RISC simples.

Gostaria de reiterar as conclusões da semana de trabalho mais recente :

  • Hoje, pelo que sabemos, basicamente não há documentação para esse recurso. Isso inclui componentes internos do LLVM e tudo.
  • Não temos, até onde sabemos, nenhuma garantia de estabilidade do LLVM. Por tudo que sabemos, a implementação do assembly embutido no LLVM pode mudar a qualquer dia.
  • Este é, atualmente, um recurso com muitos bugs no rustc. É repleto de (em tempo de compilação) segfaults, ICEs e erros estranhos de LLVM.
  • Sem uma especificação, é quase impossível imaginar um back-end alternativo para isso.

Para mim, esta é a definição de "se estabilizarmos agora vamos garantir que nos arrependamos no futuro", e não apenas "me arrepender", mas parece muito provável que "causa sérios problemas para implementar qualquer novo sistema".

No mínimo absoluto, eu acredito firmemente que o bullet (2) não pode ser comprometido (também conhecido como a definição de estável em "canal estável"). Os outros itens seriam bastante tristes se fossem abandonados, pois isso corrói a qualidade esperada do compilador Rust, que atualmente é bastante alta.

@jcranmer escreveu:

O LLVM faz um trabalho surpreendentemente ruim na prática de realmente identificar quais registros são destruídos, particularmente em instruções não geradas pelo LLVM. Isso significa que é praticamente necessário que o usuário especifique manualmente quais registros serão eliminados.

Eu pensaria que, na prática, seria muito difícil inferir listas de clobber. Só porque um fragmento de linguagem de máquina usa um registro, não significa que ele o destrua; talvez ele o salve e o restaure. Abordagens conservadoras podem desencorajar o gerador de código de usar registradores que seriam adequados para uso.

@alexcrichton escreveu:

Não temos, até onde sabemos, nenhuma garantia de estabilidade do LLVM. Por tudo que sabemos, a implementação do assembly embutido no LLVM pode mudar a qualquer dia.

Os documentos do LLVM garantem "Versões mais recentes podem ignorar recursos de versões mais antigas, mas não podem compilá-los incorretamente." (no que diz respeito à compatibilidade IR). Isso restringe o quanto eles podem alterar o assembly embutido e, como argumentei acima, não há realmente nenhuma substituição viável no nível do LLVM que mudaria radicalmente a semântica da situação atual (ao contrário, digamos, dos problemas contínuos em torno de poison e undef). Dizer que sua instabilidade prospectiva impede seu uso como base para um bloco Rust asm! é, portanto, um tanto desonesto. Agora, isso não quer dizer que haja outros problemas com ele (documentação deficiente, embora tenha melhorado; falha na restrição; diagnósticos inadequados; e erros em cenários menos comuns são os que vêm à mente).

Minha maior preocupação ao ler o tópico é que fazemos com que o perfeito seja inimigo do bom. Em particular, eu me preocupo que a busca por algum intermediário DSL mágico levará alguns anos para tentar arranjar uma forma utilizável para asm embutidas, conforme as pessoas descobrem que integrar analisadores de ASM e tentar fazê-los trabalhar com LLVM causa mais problemas em casos extremos.

O LLVM está realmente garantindo que eles nunca irão compilar mal um recurso cujo comportamento eles nunca especificaram? Como eles decidiriam se uma mudança foi uma compilação incorreta ou não? Pude ver nas outras partes do IR, mas parece muito o que esperar.

Eu acho que seria estranho se estabilizar no formato do LLVM quando nem mesmo o Clang faz isso.

O Clang não faz isso porque visa ser capaz de compilar o código que foi escrito para o GCC. rustc não tem esse objetivo. O formato GCC não é muito ergonômico, então, em última análise, acho que não queremos isso, mas não tenho certeza se seria melhor usá-lo por enquanto. Há muito código (noturno) por aí usando o formato Rust atual que quebraria se mudássemos para o estilo GCC, então provavelmente só vale a pena mudar se pudermos criar algo notavelmente melhor.

Pelo menos os blocos de construção devem ser bem projetados - e na minha opinião, "expr" : foo : bar : baz definitivamente não é.

Concordou. No mínimo, eu prefiro o formato LLVM bruto, onde as restrições e interferências estão todas em uma lista. Atualmente existe uma redundância, tendo que especificar o prefixo "=" e colocá-lo na lista de saída. Eu também acho que o LLVM o trata mais como uma chamada de função onde as saídas são o resultado da expressão, AFAIK a implementação atual de asm! é a única parte da ferrugem que tem parâmetros "out".

O LLVM faz um trabalho surpreendentemente ruim na prática de realmente identificar quais registros são destruídos

O AFAIK LLVM nem mesmo tenta fazer isso, pois o principal motivo para a montagem embutida é incluir algum código que o LLVM não entende. Ele apenas registra a alocação e a substituição do template sem olhar para a montagem real. (Obviamente, ele analisa a montagem real em algum estágio para gerar o código de máquina, mas acho que isso acontece mais tarde)

Se estivermos dispostos a gerar um montador externo

Não tenho certeza se pode haver uma alternativa ao uso do montador embutido integrado, porque de alguma forma você teria que fazer o LLVM alocar registradores para ele. Para montagem global, porém, um montador externo seria viável.

Com relação às mudanças mais recentes no montador embutido do LLVM, estamos no mesmo barco que o Clang. Ou seja, se eles fizerem algumas alterações, só temos que contorná-los quando acontecerem.

Se estabilizarmos algo definido em termos do que o clang suporta, devemos chamá-lo de clang_asm !. O asm! O nome deve ser reservado para algo que foi projetado por meio de um processo RFC completo, como outros recursos importantes do Rust. #garagem de bicicletas

Eu sou totalmente a favor. +1

Há muito código (noturno) por aí usando o formato Rust atual que quebraria se mudássemos para o estilo GCC, então provavelmente só vale a pena mudar se pudermos criar algo notavelmente melhor.

@parched Seguindo a sugestão de @jimblandy citada acima, qualquer pessoa que usar asm! ficará feliz em poder usá-lo.

Hoje, pelo que sabemos, basicamente não há documentação para esse recurso. Isso inclui componentes internos do LLVM e tudo.

Se a sintaxe de assembly do GCC realmente não for especificada ou documentada após 30 anos, então parece seguro assumir que a produção de uma sublinguagem de assembly documentada é uma tarefa que é tão difícil que está além da capacidade de Rust de realizar dados nossos recursos limitados, ou que pessoas que desejam usar o assembly simplesmente não se importam.

Não temos, até onde sabemos, nenhuma garantia de estabilidade do LLVM. Por tudo que sabemos, a implementação do assembly embutido no LLVM pode mudar a qualquer dia.

Parece improvável que a implementação do assembly embutido do GCC / Clang venha a mudar, uma vez que isso quebraria todo o código C escrito desde os anos 90.

Sem uma especificação, é quase impossível imaginar um back-end alternativo para isso.

Correndo o risco de ser insensível, a perspectiva de back-ends alternativos é discutível se Rust como uma linguagem não sobreviver devido à sua constrangedora incapacidade de entrar em montagem. Nightly não é suficiente, a menos que alguém queira tacitamente endossar a ideia de que Nightly é Rust, o que faz mais para minar a garantia de estabilidade de Rust do que a perspectiva de mudanças no LLVM.

Os outros itens seriam bastante tristes se fossem abandonados, pois isso corrói a qualidade esperada do compilador Rust, que atualmente é bastante alta.

Não estou mentindo quando digo que todos os dias sou grato pela atitude dos desenvolvedores do Rust e pelo enorme padrão de qualidade que eles mantêm (na verdade, às vezes eu desejo que vocês abrandem para que possam manter essa qualidade sem se queimar como Brian fez). No entanto, falando como alguém que estava aqui quando luqmana adicionou a macro asm! quatro anos atrás , e que não observou nenhum progresso desde então em estabilizá-la, e que está triste que a criptografia em Rust ainda seja impossível e que SIMD no Rust nem mesmo tem uma solução alternativa enquanto a interface de plataforma cruzada está sendo lentamente determinada, me sinto desanimado. Se pareço enfático aqui é porque vejo essa questão como existencial para a sobrevivência do projeto. Pode não ser uma crise neste momento, mas levará tempo para estabilizar qualquer coisa, e não temos os anos que levará para projetar e implementar um dialeto de montagem de classe mundial do zero (comprovado pelo fato que não fizemos nenhum progresso nesse sentido nos últimos quatro anos). A ferrugem precisa de uma montagem em linha estável em 2018. Precisamos da técnica anterior para fazer isso. A situação macro_rules! reconheceu que às vezes pior é melhor. Mais uma vez, estou implorando a alguém que prove que estou errado.

FWIW e chegando atrasado na festa eu gosto do que @florob a palestra sobre colônia propôs. Para aqueles que não assistiram, este é o ponto principal:

// Add 5 to variable:
let mut var = 0;
unsafe {
    asm!("add $5, {}", inout(reg) var);
}

// Get L1 cache size
let ebx: i32;
let ecx: i32;
unsafe {
    asm!(r"
        mov $$4, %eax;
        xor %ecx, %ecx;
        cpuid;
        mov %ebx, {};",
        out(reg) ebx, out(ecx) ecx, clobber(eax, ebx, edx)
    );
}
println!("L1 Cache: {}", ((ebx >> 22) + 1)
    * (((ebx >> 12) & 0x3ff) + 1)
    * ((ebx & 0xfff) + 1) * (ecx + 1));

Que tal a seguinte estratégia: renomear asm atual para llvm_asm (mais talvez algumas pequenas alterações) e declarar que seu comportamento é um detalhe de implementação do LLVM, portanto, a garantia de estabilidade do Rust não se estende totalmente a ele? O problema de back-ends diferentes deve ser mais ou menos resolvido com a funcionalidade target_feature like para compilação condicional dependendo do back-end usado. Sim, tal abordagem irá atrapalhar um pouco a estabilidade do Rust, mas manter a montagem no limbo dessa maneira é prejudicial para o Rust em sua própria maneira.

Publiquei um pré-RFC com uma proposta de sintaxe alternativa no fórum interno: https://internals.rust-lang.org/t/pre-rfc-inline-assembly/6443 . Feedback bem-vindo.

Parece-me que o melhor é definitivamente o inimigo do tipo-ok aqui. Eu apoio totalmente a colocação de uma macro gcc_asm! ou clang_asm! ou llvm_asm! (ou qualquer subconjunto apropriado dela) em estável com sintaxe e semântica compatíveis por enquanto, enquanto uma solução melhor é desenvolvida . Não vejo o suporte para sempre como uma grande carga de manutenção: os sistemas mais sofisticados propostos acima parecem que facilmente suportariam apenas transformar as macros do estilo antigo em sacarinas sintáticas para o novo.

Eu tenho um programa binário http: //[email protected]/BartMassey/popcount que requer montagem em linha para a instrução x86_64 popcntl . Esse assembly embutido é a única coisa que mantém esse código em funcionamento todas as noites. O código foi derivado de um programa C de 12 anos.

No momento, minha montagem está condicionada a

    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]

e obtém a informação cpuid para ver se popcnt está presente. Seria bom ter algo em Rust semelhante à recente biblioteca cpu_features do Google https://opensource.googleblog.com/2018/02/cpu-features-library.html em Rust, mas c'est la vie.

Como este é um programa de demonstração mais do que qualquer outra coisa, gostaria de manter o assembly embutido. Para programas reais, o count_ones() intrínseco seria suficiente - exceto que fazê-lo usar popcntl requer a passagem de "-C target-cpu = native" para Cargo, provavelmente por RUSTFLAGS (veja a edição 1137 e várias questões relacionadas), já que distribuir .cargo/config com minha fonte não parece uma boa ideia, o que significa que agora eu tenho um Makefile chamando Cargo.

Em suma, seria bom se alguém pudesse usar as instruções sofisticadas de contagem de pop da Intel e de outros em aplicativos reais, mas parece mais difícil do que precisa ser. Os intrínsecos não são inteiramente a resposta. Atual asm! é uma resposta ok se disponível no estável. Seria ótimo ter uma sintaxe e semântica melhores para assembly embutido, mas eu realmente não preciso disso. Seria ótimo poder especificar target-cpu=native diretamente em Cargo.toml , mas isso não resolveria realmente meu problema.

Desculpe, divagando. Apenas pensei em compartilhar por que me preocupo com isso.

@BartMassey Não entendo, por que você precisa tão desesperadamente compilar para popcnt? A única razão que posso ver é desempenho e IMO, você definitivamente deve usar count_ones () nesse caso. O que você está procurando não é conjunto embutido, mas target_feature (rust-lang / rfcs # 2045) para que você possa dizer ao compilador que é permitido emitir popcnt.

@BartMassey você nem precisa usar o assembly embutido para isso, apenas use coresimd cfg_feature_enabled!("popcnt") para consultar se a cpu em que seu binário é executado suporta a instrução popcnt (ela irá resolva isso em tempo de compilação, se possível).

coresimd também fornece um popcnt intrínseco que é garantido para usar a instrução popcnt .

@gnzlbg

coresimd também fornece um popcnt intrínseco que garante o uso da instrução popcnt.

É um pouco fora do assunto, mas esta afirmação não é estritamente verdadeira. _popcnt64 usa leading_zeros sob o capô, portanto, se popcnt recurso não for habilitado pelo usuário da caixa e o autor da caixa esquecerá de usar #![cfg(target_feature = "popcnt")] esse recurso intrínseco obterá compilado em uma montagem ineficaz e não há salvaguardas contra isso.

portanto, se o recurso popcnt não for habilitado pelo usuário da caixa

Isso é incorreto, pois o intrínseco usa o atributo #[target_feature(enable = "popcnt")] para habilitar o recurso popcnt para o intrínseco incondicionalmente, independentemente do que o usuário da caixa habilita ou desabilita. Além disso, o atributo assert_instr(popcnt) garante que o intrínseco seja desmontado em popcnt em todas as plataformas x86 suportadas pelo Rust.

Se alguém estiver usando o Rust em uma plataforma x86 que o Rust não suporta atualmente, cabe a quem está portando core garantir que esses intrínsecos gerem popcnt naquele alvo.


EDITAR: @newpavlov

portanto, se o recurso popcnt não for habilitado pelo usuário do crate e o autor do crate esquecerá de usar #! [cfg (target_feature = "popcnt")] este intrínseco será compilado em uma montagem ineficaz e não há salvaguardas contra isso.

Pelo menos no exemplo que você mencionou no problema, fazer isso introduz um comportamento indefinido no programa e, neste caso, o compilador tem permissão para fazer qualquer coisa. Codegen ruim que funciona é um dos vários resultados que podemos obter.

Em primeiro lugar, desculpas por ter atrapalhado a discussão. Só queria reiterar meu ponto principal, que era "Eu apoio totalmente a colocação de um gcc_asm! Ou clang_asm! Ou llvm_asm! Macro (ou qualquer subconjunto apropriado dele) em estável com sintaxe e semântica compatíveis por enquanto, enquanto uma solução melhor é encontrada. "

O ponto da montagem em linha é que este é um benchmark / demonstração popcount. Quero uma instrução popcntl verdadeira garantida, quando possível, tanto como linha de base quanto para ilustrar como usar o assembly embutido. Também quero garantir que count_ones() use uma instrução popcount quando possível, para que Rustc não pareça terrível em comparação com GCC e Clang.

Obrigado por apontar target_feature=popcnt . Vou pensar em como usá-lo aqui. Acho que quero testar count_ones() independentemente de qual CPU o usuário está compilando e independentemente de haver uma instrução popcount. Eu só quero ter certeza de que, se a CPU de destino tiver contagem de pop-ups count_ones() use-a.

As caixas de stdsimd / coresimd parecem boas e provavelmente devem ser habilitadas para esses benchmarks. Obrigado! Para este aplicativo, eu prefiro usar o menos possível fora dos recursos de linguagem padrão (já estou me sentindo culpado por lazy_static ). No entanto, essas instalações parecem boas demais para serem ignoradas e parece que estão no caminho certo para se tornarem "oficiais".

Existe uma ideia flutuada por @nbp onde poderia haver alguma implementação que vai de alguma representação de código para bytes de máquina (poderia ser uma caixa proc-macro ou algo assim?) E então esses bytes são incluídos diretamente no local específico no código.

Emendar bytes de código arbitrários em lugares arbitrários dentro de uma função parece um problema muito mais fácil de resolver (embora a capacidade de especificar entradas, saídas e suas restrições tanto quanto clobbers ainda seja necessária).

cc @eddyb

@nagisa é um pouco mais do que apenas um pedaço de código de máquina, você também deve ter cuidado com os registros de entrada, saída e clobber. Se o fragmento ASM disser que deseja uma determinada variável em% rax e que irá destruir% esi, você precisa ter certeza de que o código circundante funciona bem. Além disso, se o desenvolvedor permitir que o compilador aloque os registradores, você provavelmente desejará otimizar a alocação para evitar o derramamento e a movimentação de valores.

@simias , na verdade você terá que especificar como as variáveis ​​são associadas a registros específicos e quais registros são eliminados, mas tudo isso é menor do que padronizar qualquer linguagem assembly ou qualquer linguagem assembly LLVM.

Padronizar as sequências de bytes é provavelmente a maneira mais fácil de avançar, movendo o tipo de assembly para um driver / macro-proc.

Um problema de ter bytes textuais em vez de assembly embutido adequado, é que o compilador não teria opção para renomear alfa de registro, o que não espero que as pessoas que escrevem assembly embutido também estejam esperando.

Mas como isso funcionaria com a alocação de registro se eu quiser deixar o compilador lidar com isso? Por exemplo, usando a sintaxe (atroz) do GCC:

asm ("leal (%1, %1, 4), %0"
     : "=r" (five_times_x)
     : "r" (x));

Em algo assim, deixo o compilador alocar os registradores, esperando que ele me dê o que for mais conveniente e eficiente. Por exemplo, em x86 64 se five_time_x é o valor de retorno, então o compilador pode alocar eax e se x é um parâmetro de função pode já estar disponível em algum registro. É claro que o compilador só sabe exatamente como alocará registradores bem tarde na seqüência de compilação (especialmente se não for tão trivial quanto simplesmente params de função e valores de retorno).

Sua solução proposta funcionaria com algo assim?

@nbp Devo dizer que estou um pouco confuso com esta proposta.
Em primeiro lugar, padronizar a linguagem assembly nunca foi algo que quiséssemos alcançar com o assembly embutido. Pelo menos para mim, a premissa sempre foi que a linguagem assembly usada pelo montador do sistema seria aceita.
O problema não é fazer com que o assembly seja analisado / montado, podemos passar isso para o LLVM facilmente.
O problema é preencher o assembly modelado (ou fornecer ao LLVM as informações necessárias para fazê-lo) e especificar entradas, saídas e sobrescritos.
O último problema não é realmente resolvido por sua proposta. No entanto, é aliviado, porque você não iria / não poderia suportar classes de registradores (sobre as quais @simias pergunta), mas apenas registradores concretos.
No ponto em que as restrições são simplificadas a esse ponto, é realmente tão fácil dar suporte à montagem embutida "real". O primeiro argumento é uma string contendo um assembly (não padronizado), os outros argumentos são as restrições. Isso é facilmente mapeado para as expressões assembler embutidas do LLVM.
Por outro lado, inserir bytes brutos não é, pelo que eu sei (ou posso dizer pelo Manual de Referência IR do LLVM), suportado pelo LLVM. Portanto, estaríamos basicamente estendendo o LLVM IR e reimplementando um recurso (montagem do sistema de montagem) que já está presente no LLVM usando caixas separadas.

@nbp

na verdade, você terá que especificar como as variáveis ​​são associadas a registros específicos e quais registros são eliminados, mas tudo isso é menor do que padronizar qualquer linguagem assembly ou qualquer linguagem assembly LLVM.

Então, como isso seria feito? Eu tenho uma sequência de bytes com registros codificados com basicamente significa que os registros de entrada / saída, clobbers, etc. são todos codificados dentro dessa sequência de bytes.

Agora injeto esses bytes em algum lugar do meu binário ferrugem. Como posso informar ao rustc quais registros são de entrada / saída, quais registros foram destruídos, etc.? Como isso é um problema menor para resolver do que estabilizar a montagem em linha? Parece-me que isso é exatamente o que o assembly inline faz, talvez um pouco mais difícil porque agora é necessário especificar clobbers de entrada / saída duas vezes, no assembly escrito e de qualquer maneira que passemos essas informações para rustc. Além disso, o rustc não teria um tempo fácil para validar isso, porque para isso ele precisaria ser capaz de analisar a sequência de bytes no assembly e, em seguida, inspecioná-la. o que estou perdendo?

@simias

asm ("leal (%1, %1, 4), %0"
     : "=r" (five_times_x)
     : "r" (x));

Isso não seria possível, já que o valor bruto dos bytes não permite a renomeação alfa dos registradores, e os registradores teriam que ser reforçados pela sequência de código à frente.

@Florob

Pelo menos para mim, a premissa sempre foi que a linguagem assembly usada pelo montador do sistema seria aceita.

Meu entendimento é que confiar no montador do sistema não é algo em que queremos confiar, mas mais uma falha aceita como parte do conjunto! macro. Também contando com asm! ser a sintaxe LLVM seria doloroso para o desenvolvimento de back-end adicional.

@gnzlbg

Então, como isso seria feito? Eu tenho uma sequência de bytes com registros codificados com basicamente significa que os registros de entrada / saída, clobbers, etc. são todos codificados dentro dessa sequência de bytes.

A ideia seria ter uma lista de entradas, saídas e registros clobbered, onde as entradas seriam uma tupla do nome do registro associado a uma referência ou cópia (mutável), o registro clobbered seria uma lista de nomes de registro, e a saída seria uma lista de registros de saída e formaria uma tupla de registros nomeados aos quais estão os tipos associados.

fn swap(a: u32, b: u32) -> (u32, u32) {
  unsafe{
    asm_raw!{
       bytes: [0x91],
       inputs: [(std::asm::eax, a), (std::asm::ecx, b)],
       clobbered: [],
       outputs: (std::asm::eax, std::asm::ecx),
    }
  }
}

Esta sequência de código pode ser a saída de alguma macro procedural do compilador, que pode ser semelhante a:

fn swap(a: u32, b: u32) -> (u32, u32) {
  unsafe{
    asm_x64!{
       ; <-- (eax, a), (ecx, b)
       xchg eax, ecx
       ; --> (eax, ecx)
    }
  }
}

Essas sequências, não serão capazes de incorporar diretamente nenhum símbolo ou endereço e teriam que ser computadas e fornecidas como registradores. Tenho certeza de que podemos descobrir como adicionar a capacidade de inserir alguns endereços de símbolo dentro da sequência de bytes mais tarde.

A vantagem dessa abordagem é que apenas a lista de registradores e restrições precisam ser padronizadas, e isso é algo que seria facilmente suportado por qualquer futuro back-end.

@nbp

Meu entendimento é que confiar no montador do sistema não é algo em que queremos confiar, mas mais uma falha aceita como parte do conjunto! macro. Também contando com asm! ser a sintaxe LLVM seria doloroso para o desenvolvimento de back-end adicional.

Não acho que seja uma avaliação precisa? Com a pequena exceção das duas sintaxes diferentes para o assembly x86, a sintaxe do assembly é amplamente padrão e portátil. O único problema com o montador de sistema pode ser a falta de instruções mais recentes, mas essa é uma situação de nicho para a qual não vale a pena otimizar.

O problema real é a cola na alocação de registros. Mas, no que diz respeito à própria string de montagem em si, isso apenas significa que alguém tem que fazer algumas coisas de substituição de string e talvez alguma análise - e esse tipo de substituição deve estar trivialmente disponível para qualquer back-end putativo.

Eu concordo que a sintaxe do LLVM (ou gcc) para este material é uma porcaria, mas mover para bytes pré-compilados significa que qualquer asm crate agora precisa instalar um assembler completo e possivelmente um alocador de registro completo (ou fazer os programadores alocarem manualmente os registros), ou tentar para usar o montador do sistema. Nesse ponto, não parece que ele está realmente agregando muito valor.

@jcranmer

... mas mover para bytes pré-compilados significa que qualquer asm crate agora precisa instalar um montador completo e possivelmente um alocador de registro completo (ou fazer os programadores alocarem os registros manualmente) ou tentar usar o montador do sistema

https://github.com/CensoredUsername/dynasm-rs

Esta caixa usa macro manipulada por um plugin para montar o código de montagem e gerar vetores de código de montagem bruto para serem concatenados no tempo de execução.

@nbp talvez meus casos de uso sejam peculiares, mas a falta de renomeação de registro e deixar o compilador alocar registros para mim seria um problema, porque significa que preciso ter muita sorte com minha escolha de registros e acontecer de " clique para a direita "ou o compilador terá que emitir um código não ideal para embaralhar os registros de acordo com minhas convenções arbitrárias.

Se o assembly blob não se integrar bem com o assembly emitido pelo compilador circundante, posso também fatorar o stub ASM em um método externo de estilo C em um arquivo assembly .s autônomo, uma vez que as chamadas de função têm o mesmo tipo de registro - restrições de alocação. Isso já funciona hoje, embora eu suponha que tê-lo integrado ao rustc pode simplificar o sistema de compilação em comparação a ter um arquivo de montagem autônomo. Acho que o que estou dizendo é que a sua proposta da IMO não nos leva muito longe em comparação com a situação atual.

E se o código ASM chamar símbolos externos que seriam resolvidos pelo vinculador? Você precisa passar essas informações, já que não é possível resolvê-las até o final do processo de compilação. Você teria que passar essa referência ao lado de sua matriz de bytes e deixar o vinculador resolvê-los muito mais tarde.

@jcranmer

Com a pequena exceção das duas sintaxes diferentes para o assembly x86, a sintaxe do assembly é amplamente padrão e portátil.

Não tenho certeza se entendi o que você quis dizer com isso, obviamente, a sintaxe ASM não é portátil entre arquiteturas. E mesmo dentro da mesma arquitetura, muitas vezes há variações e opções que mudam a forma como a linguagem é montada.

Posso dar o MIPS como exemplo, existem dois sinalizadores de configuração importantes que ajustam o comportamento do assembler: at e reorder . at diz se o montador tem permissão para usar implicitamente o registrador AT (assembler temporário) ao montar certas pseudo-instruções. O código que usa explicitamente AT para armazenar dados deve ser montado com at ou ele quebrará.

reorder define se o codificador manipula manualmente os slots de atraso de ramificação ou se ele confia no montador para lidar com eles. Montar código com a configuração reorder errada quase certamente gerará um código de máquina falso. Ao escrever um assembly MIPS, você deve estar ciente do modo atual o tempo todo se ele contiver alguma instrução de ramificação. Por exemplo, é impossível saber o significado desta lista MIPS se você não souber se reorder está habilitado:

    addui   $a0, 4
    jal     some_func
    addui   $a1, $s0, 3

A montagem ARM de 32 bits tem as variações Thumb / ARM, é importante saber qual conjunto de instruções você está almejando (e você pode mudar rapidamente nas chamadas de função). A mistura dos dois conjuntos deve ser feita com muito cuidado. O código ARM também normalmente carrega grandes valores imediatos usando uma carga implícita relativa ao PC, se você pré-montar seu código, terá que ser cuidadoso sobre como passar esses valores, uma vez que eles devem permanecer próximos, mas não são instruções reais com um local bem definido. Estou falando de pseudo-instruções como:

   ldr   r2, =0x89abcdef

O MIPS, por outro lado, tende a dividir o valor imediato em dois valores de 16 bits e usar uma combinação lui / ori ou lui / andi. Geralmente está escondido atrás das li / la pseudo-instruções, mas se você está escrevendo código com noreorder e não quer desperdiçar o slot de atraso, às vezes você tem que lidar à mão, o que resulta em um código de aparência engraçada:

.set noreorder

   /* Display a message using printf */
   lui $a0, %hi(hello)
   jal printf
   ori $a0, %lo(hello)

.data

hello:
.string "Hello, world!\n"

As construções %hi e %lo são uma maneira de dizer ao assembly para gerar uma referência para os 16 bits superior e inferior do símbolo hello respectivamente.

Alguns códigos precisam de restrições de alinhamento muito peculiares (comuns quando você está lidando com código de invalidação de cache, por exemplo, você precisa ter certeza de que não vai serrar o branch em que está sentado). E há o problema de lidar com símbolos externos que não podem ser resolvidos neste ponto do processo de compilação, como mencionei anteriormente.

Tenho certeza de que poderia criar peculiaridades para um monte de outras arquiteturas com as quais estou menos familiarizado. Por essas razões, não tenho certeza se estou muito otimista quanto à abordagem macro / DSL. Eu entendo que ter uma literal de string opaca aleatória no meio do código não é muito elegante, mas eu realmente não vejo o que a integração da sintaxe ASM completa em ferrugem de uma forma ou de outra nos daria, exceto dores de cabeça adicionais ao adicionar suporte para uma nova arquitetura.

Escrever um assembler é algo que pode parecer trivial à primeira vista, mas pode se tornar muito complicado se você quiser suportar todos os sinos, assobios e peculiaridades de todas as arquiteturas por aí.

Por outro lado, ter uma boa maneira de especificar ligações e clobbers seria extremamente valioso (em comparação com a sintaxe ... perfectible do gcc).

Oi, pessoal,

Desculpe incomodá-lo, eu só queria diminuir meus dois centavos, porque sou apenas um usuário, e muito tímido / quieto, ah, e um recém-chegado, acabei de pousar em Rust, mas já estou apaixonado por isso.

Mas essa coisa de montagem é uma loucura, quer dizer, é uma conversa de três anos, com um monte de ideias e reclamações, mas nada que pareça um consenso mínimo. Três anos e não um RFC, parece um pouco o fim da morte. Estou desenvolvendo uma humilde biblioteca matemática (que espero se materializar em duas ou três caixas), e para mim (e suspeito que para qualquer outro colega interessado em escrever montagem em ferrugem), o mais importante é realmente ser capaz de faça! com uma garantia mínima de que nada vai mudar no dia seguinte (é o que me faz sentir o canal instável, e principalmente essa conversa).

Eu entendo que todos aqui querem a melhor solução, e talvez um dia alguém saia com essa, mas por hoje eu acredito que a macro atual está bem (bem, talvez um pouco restritiva em alguns aspectos, mas espero que nada que não possa ser tratada de forma incremental). Escrever assembly é como a coisa mais importante em uma linguagem de sistema, um recurso muito necessário, e embora eu esteja bem em confiar no cpp_build até que isso seja corrigido, tenho muito medo de que se demorar muito mais tempo ele se tornará uma dependência para sempre. Não sei porque, chame de ideia irracional, mas acho que ter que chamar cpp para chamar assembly é um pouco triste, quero uma solução de ferrugem pura.

FWIW Rust não é tão especial aqui, MSVC não tem asm linha para x86_64 quer. Eles têm uma implementação realmente estranha em que você pode usar variáveis ​​como operandos, mas funciona apenas para x86.

@josevalaad Você poderia falar mais sobre para que está usando o assembly inline?

Normalmente, só o vemos usado em situações semelhantes ao sistema operacional, que normalmente ficam paralisadas todas as noites por outros motivos também e, mesmo assim, quase não usam asm! , portanto, estabilizar asm! não foi um prioridade alta o suficiente para projetar e desenvolver algo que possa sobreviver adequadamente fora do LLVM e agradar a todos.

Além disso, muitas coisas podem ser feitas usando os intrínsecos da plataforma exposta. x86 e x86_64 foram estabilizados e outras plataformas estão em andamento. A expectativa da maioria das pessoas é que eles atinjam 95-99% das metas. Você pode ver meu próprio caixote jetscii como um exemplo do uso de alguns dos intrínsecos.

Acabamos de mesclar um PR jemalloc que usa assembly embutido para contornar bugs de geração de código no LLVM - https://github.com/jemalloc/jemalloc/pull/1303 . Alguém usou o assembly embutido neste problema (https://github.com/rust-lang/rust/issues/53232#issue-349262078) para contornar um bug de geração de código em Rust (LLVM) que aconteceu na caixa jetscii. Ambos aconteceram nas últimas duas semanas e, em ambos os casos, os usuários tentaram com o intrínseco, mas o compilador falhou.

Quando a geração de código para um compilador C passa a ser inaceitável, na pior das hipóteses, o usuário pode usar o assembly embutido e continuar trabalhando em C.

Quando isso acontece no Rust estável, agora temos que dizer às pessoas para usar uma linguagem de programação diferente ou esperar um período de tempo indeterminado (geralmente da ordem de anos). Isso não é legal.

@eddyb Bem, estou escrevendo uma pequena biblioteca de álgebra matricial. Dentro dessa biblioteca, estou implementando o BLAS, talvez algumas rotinas LAPACK (ainda não lá) no Rust, porque eu queria que a biblioteca fosse uma implementação pura do Rust. Não é nada sério ainda, mas de qualquer forma, eu queria que o usuário pudesse optar por um pouco de velocidade e diversão asm, especialmente com a operação GEMM, que costumava ser essencial (o mais usado, de qualquer maneira, e se você seguir a abordagem de pessoas do BLIS é tudo o que você precisa), pelo menos em x86 / x86_64. E essa é a história completa. Obviamente, posso usar o canal noturno também, só queria empurrar um pouco na direção pragmática de estabilização do recurso.

@shepmaster Existem _plenty_ de casos de uso para os quais os intrínsecos não são suficientes. No topo da minha cabeça de coisas recentes onde pensei "por que, oh, por que Rust não tem conjunto estável?", Não há intrínsecos do XACQUIRE / XRELEASE.

O conjunto estável em linha é crítico e não, os intrínsecos não são suficientes.

Meu ponto original era tentar ajudar alguém a ter a habilidade de escrever código mais rápido. Eles não fizeram menção de saber que os intrínsecos estavam mesmo disponíveis, e isso é tudo que procurei compartilhar. O resto eram informações básicas.

Eu nem mesmo estou defendendo um ponto de vista específico, então, por favor, não tente discutir comigo - eu não tenho interesse nesta corrida. Estou simplesmente repetindo qual é o ponto de vista atual da forma como o entendo . Eu participo de um projeto que requer assembly inline que é altamente improvável de ter intrínsecos em um futuro próximo, então também estou interessado em alguma quantidade de assembly inline estável, mas nightly assembly não me incomoda indevidamente, nem invoca um assembler.

Sim, há casos que precisam de montagem por enquanto e há casos que vão precisar para sempre, eu disse originalmente (ênfase adicionada para maior clareza):

A expectativa da maioria das pessoas é que [o intrínseco] cumprirá 95-99% das metas .

É minha opinião que se você quiser ver uma montagem estável, alguém (ou um grupo de pessoas) vai precisar obter um consenso geral da equipe Rust sobre uma direção para começar e então se esforçar para atualizá-la .

Não é nada sério ainda, mas de qualquer forma, eu queria que o usuário pudesse optar por um pouco de velocidade e diversão asm, especialmente com a operação GEMM, que costumava ser essencial (o mais usado, de qualquer maneira, e se você seguir a abordagem de pessoas do BLIS é tudo o que você precisa), pelo menos em x86 / x86_64.

Ainda não entendo quais instruções você precisa para acessar e que não consegue sem a montagem embutida. Ou é apenas uma sequência específica de instruções aritméticas?
Em caso afirmativo, você comparou uma fonte de Rust equivalente com o assembly em linha?

quais instruções você precisa para acessar e que não pode sem a montagem embutida

Bem, quando você está falando sobre montagem em matemática, você basicamente está falando sobre o uso de registros SIMD e instruções como _mm256_mul_pd, _mm256_permute2f128_pd, etc. e operações de vetorização onde ele continua. O fato é que você pode adotar diferentes abordagens para a vetorização e, geralmente, é uma pequena tentativa e erro até que você obtenha um desempenho otimizado para o processador que você tem como objetivo e o uso que tem em mente. Normalmente, no nível da biblioteca, você primeiro precisa consultar o processador injetando código de conjunto para saber o conjunto de instruções e registros suportados e, em seguida, compilar condicionalmente uma versão específica de seu kernel de conjunto de matemática.

Em caso afirmativo, você comparou uma fonte de Rust equivalente com o assembly em linha?

No momento não tenho nenhum teste específico em mãos e estou de férias, então prefiro não me envolver muito com isso, mas sim, se você me der algumas semanas, posso postar um comparativo de desempenho. Em qualquer caso, costumava ser impossível para o compilador produzir código o mais rápido possível com a montagem ajustada manualmente. Não é possível pelo menos em C, mesmo se você usar as técnicas clássicas de desempenho, como desenrolamento manual de loop quando necessário, etc., então imagino que não seja possível no Rust.

Taylor Cramer sugeriu que eu postasse aqui. Perdoe-me, pois não li todos os comentários para compreender o estado atual da discussão; esta é apenas uma voz de apoio e declaração de nossa situação.

Para um projeto bare-metal no Google, adoraríamos ver algum movimento na estabilização do montador embutido e em nível de módulo. A alternativa é usar o FFI para chamar funções escritas em assembly puro e montadas separadamente e vinculadas em um binário.

Poderíamos definir funções em assembler e chamá-las via FFI, vinculando-as em uma etapa separada, mas não conheço nenhum projeto sério de bare-metal que faça isso exclusivamente, pois tem desvantagens em termos de complexidade e desempenho. Redox usa 'asm!'. Os suspeitos usuais de Linux, BSDs, macOS, Windows, etc, fazem uso abundante do assembler embutido. Zircão e seL4 fazem isso. Até mesmo o Plano 9 desistiu disso alguns anos atrás, na bifurcação de Harvey.

Para coisas de desempenho crítico, a sobrecarga da chamada de função pode dominar, dependendo da complexidade da função chamada. Em termos de complexidade, definir funções de assembler separadas apenas para invocar uma única instrução, ler ou escrever um registro ou de outra forma manipular o estado da máquina que normalmente está oculto de um programador de espaço do usuário significa mais tedioso clichê para errar. Em qualquer caso, teríamos que ser mais criativos em nosso uso do Cargo (ou suplementar com um sistema de construção externo ou um script de shell ou algo assim) para fazer isso. Talvez build.rs possa ajudar aqui, mas alimentá-lo no vinculador parece mais desafiador.

Eu também gostaria muito se houvesse alguma maneira de inserir os valores das constantes simbólicas no modelo do assembler.

adoraríamos ver algum movimento na estabilização do montador embutido e em nível de módulo.

O último pré-RFC (https://internals.rust-lang.org/t/pre-rfc-inline-assembly/6443) alcançou consenso 6 meses atrás (pelo menos na maioria das questões fundamentais), então a próxima etapa é enviar um RFC baseado nisso. Se você quiser que isso aconteça mais rápido, eu recomendo entrar em contato com @Florob sobre isso.

Pelo que vale a pena, eu preciso de acesso direto aos registros FSGS para obter o ponteiro para a estrutura TEB no Windows, também preciso de um intrínseco _bittest64 semelhante para aplicar bt a um local de memória arbitrário, nenhum dos quais eu poderia encontrar uma maneira de fazer sem assembly embutido ou chamadas externas.

O terceiro ponto mencionado aqui me preocupa, entretanto, já que o LLVM de fato prefere Just Crash se algo estiver errado, fornecendo nenhuma mensagem de erro.

@MSxDOS

Eu também preciso de um intrínseco semelhante ao _bittest64 para aplicar bt a um local de memória arbitrário, nenhum dos quais eu poderia encontrar uma maneira de fazer sem assembly embutido ou chamadas externas.

Não deve ser difícil adicionar aquele a stdsimd , o clang os implementa usando assembly embutido (https://github.com/llvm-mirror/clang/blob/c1c07cca8cae5f924cedaac7b202b0f3c167111d/test/CodeGen/bittest-intrin .c # L45) mas podemos usar isso na biblioteca std e expor o intrínseco ao Rust seguro.

Sinta-se encorajado a abrir um problema no repositório stdsimd sobre os intrínsecos ausentes.

@josevalaad

Bem, quando você está falando sobre montagem em matemática, você basicamente está falando sobre o uso de registros SIMD e instruções como _mm256_mul_pd, _mm256_permute2f128_pd, etc. e operações de vetorização onde ele continua.

Ah, eu suspeitei que poderia ser o caso. Bem, se você quiser tentar, pode traduzir o assembly em std::arch chamadas intrínsecas e ver se obtém o mesmo desempenho com isso.

Caso contrário , registre os problemas. LLVM não é mágico, mas pelo menos intrínseco deve ser tão bom quanto asm.

@dancrossnyc Se você não se importa que eu pergunte, há algum caso de uso / recurso de plataforma em particular que exija montagem em linha, na sua situação?

@MSxDOS Talvez devêssemos expor os intrínsecos para a leitura dos registradores de "segmento"?


Talvez devêssemos fazer alguma coleta de dados e obter um detalhamento do que as pessoas realmente desejam asm! , e ver quantos deles poderiam ser suportados de alguma outra forma.

Talvez devêssemos fazer alguma coleta de dados e obter um detalhamento do que as pessoas realmente desejam!

Eu quero asm! por:

  • trabalhando em torno de intrínsecos não fornecidos pelo compilador
  • contornar bugs do compilador / geração de código abaixo do ideal
  • realizar operações que não podem ser realizadas por meio de uma sequência de chamadas intrínsecas únicas, por exemplo, um EFLAGS de leitura EFLAGS-modificação-gravação onde o LLVM tem permissão para modificar eflags entre a leitura e a gravação, e onde o LLVM também assume que o usuário não modificará isso pelas costas (ou seja, a única maneira de trabalhar com segurança com EFLAGS é escrever as operações de leitura-modificação-gravação como um único bloco atômico asm! ).

e ver quantos deles poderiam ser suportados de alguma outra maneira.

Não vejo nenhuma outra maneira de oferecer suporte a qualquer um desses casos de uso que não envolva alguma forma de montagem embutida, mas minha mente está aberta.

Copiado de minha postagem no thread pré-RFC, aqui está um assembly embutido (ARM64) que estou usando em meu projeto atual:

// Common code for interruptible syscalls
macro_rules! asm_interruptible_syscall {
    () => {
        r#"
            # If a signal interrupts us between 0 and 1, the signal handler
            # will rewind the PC back to 0 so that the interrupt flag check is
            # atomic.
            0:
                ldrb ${0:w}, $2
                cbnz ${0:w}, 2f
            1:
               svc #0
            2:

            # Record the range of instructions which should be atomic.
            .section interrupt_restart_list, "aw"
            .quad 0b
            .quad 1b
            .previous
        "#
    };
}

// There are other versions of this function with different numbers of
// arguments, however they all share the same asm code above.
#[inline]
pub unsafe fn interruptible_syscall3(
    interrupt_flag: &AtomicBool,
    nr: usize,
    arg0: usize,
    arg1: usize,
    arg2: usize,
) -> Interruptible<usize> {
    let result;
    let interrupted: u64;
    asm!(
        asm_interruptible_syscall!()
        : "=&r" (interrupted)
          "={x0}" (result)
        : "*m" (interrupt_flag)
          "{x8}" (nr as u64)
          "{x0}" (arg0 as u64)
          "{x1}" (arg1 as u64)
          "{x2}" (arg2 as u64)
        : "x8", "memory"
        : "volatile"
    );
    if interrupted == 0 {
        Ok(result)
    } else {
        Err(Interrupted)
    }
}

@Amanieu nota que @japaric está trabalhando em direção aos intrínsecos para ARM . Vale a pena verificar se essa proposta atende às suas necessidades.

@shepmaster

@Amanieu nota que @japaric está trabalhando em direção aos intrínsecos para ARM. Vale a pena verificar se essa proposta atende às suas necessidades.

Vale ressaltar que:

  • este trabalho não substitui a montagem embutida, apenas a complementa. Esta abordagem implementa APIs de fornecedores em std::arch , essas APIs já são insuficientes para algumas pessoas.

  • esta abordagem só é utilizável quando uma sequência de chamadas intrínsecas como foo(); bar(); baz(); produz código indistinguível daquela sequência de instruções - este não é necessariamente o caso, e quando não é, o código que parece correto produz, na melhor das hipóteses, incorreto resultados e, na pior das hipóteses, tem comportamento indefinido (já tínhamos bugs devido a isso em x86 e x86_64 em std , por exemplo, https://github.com/rust- lang-Nursery / stdsimd / blob / master / coresimd / x86 / cpuid.rs # L108 - outras arquiteturas também têm esses problemas).

  • alguns intrínsecos têm argumentos de modo imediato, que você não pode passar por meio de uma chamada de função, de forma que foo(3) não funcionará. Cada solução para esse problema é atualmente uma solução alternativa maluca e, em alguns casos, nenhuma solução alternativa é possível no Rust, portanto, simplesmente não fornecemos alguns desses intrínsecos.

Portanto, se as APIs do fornecedor são implementáveis ​​em Rust, disponível em std::arch , e podem ser combinadas para resolver um problema, concordo que elas são melhores do que a montagem embutida. Mas de vez em quando as APIs não estão disponíveis, talvez nem mesmo sejam implementáveis ​​e / ou não podem ser combinadas corretamente. Embora possamos corrigir os "problemas de implementabilidade" no futuro, se o que você deseja fazer não for exposto pela API do fornecedor ou se as APIs não puderem ser combinadas, esta abordagem não o ajudará.

O que pode ser muito surpreendente sobre a implementação de intrínsecos do LLVM (especialmente SIMD) é que eles não estão em conformidade com o mapeamento explícito de intrínsecos para instruções da Intel - eles estão sujeitos a uma ampla gama de otimizações do compilador. Por exemplo, lembro-me de uma vez em que tentei reduzir a pressão da memória calculando algumas constantes de outras constantes em vez de carregá-las da memória. Mas o LLVM simplesmente continuou a dobrar constantemente a coisa toda de volta para a carga de memória exata que eu estava tentando evitar. Em um caso diferente, eu queria investigar a substituição de um shuffle de 16 bits por um shuffle de 8 bits para reduzir a pressão da porta 5. Ainda assim, em sua sabedoria infinita, o sempre útil otimizador LLVM percebeu que meu shuffle de 8 bits é na verdade um shuffle de 16 bits e o substituiu.

Ambas as otimizações certamente geram melhor rendimento (especialmente em face do hyperthreading), mas não a redução de latência que eu esperava alcançar. Eu acabei caindo no nasm para aquele experimento, mas ter que reescrever o código do intrínseco para o asm simples foi apenas um atrito desnecessário. Claro que quero que o otimizador lide com coisas como seleção de instruções ou dobramento constante ao usar alguma API vetorial de alto nível. Mas quando decidi explicitamente quais instruções usar, realmente não quero que o compilador mexa nisso. A única alternativa é o conjunto em linha.

Portanto, se as APIs do fornecedor são implementáveis ​​em Rust, disponível em std::arch , e podem ser combinadas para resolver um problema, concordo que são melhores do que o assembly inline

Isso é tudo que eu tenho dito no início

cumprir 95-99% das metas

e de novo

Sim, há casos que precisam de montagem por enquanto e há casos que vão precisar para sempre, eu disse originalmente (ênfase adicionada para maior clareza):

A expectativa da maioria das pessoas é que [o intrínseco] cumpra 95-99% das metas.

Isso é a mesma coisa que @eddyb está dizendo em paralelo. Não estou claro por que várias pessoas estão agindo como se eu estivesse desconsiderando completamente a utilidade da montagem embutida enquanto tento apontar a realidade da situação atual .

Eu tenho

  1. Apontou um pôster que não fez menção de saber que existiam intrínsecos em relação aos intrínsecos estáveis ​​de hoje .
  2. Apontou outro pôster para os intrínsecos propostos para que eles pudessem fornecer feedback antecipado sobre a proposta.

Deixe-me dizer isso muito claramente: sim, a montagem embutida às vezes é necessária e boa . Eu não estou discutindo isso. Estou apenas tentando ajudar as pessoas a resolver problemas do mundo real com as ferramentas que estão disponíveis agora.

O que eu estava tentando dizer é que deveríamos ter uma abordagem mais organizada para isso, uma pesquisa adequada, e reunir muito mais dados do que poucos de nós neste tópico e, em seguida, usar isso para apontar as necessidades mais comuns de assembly embutido (já que está claro que intrínsecos não podem substituí-lo totalmente).

Suspeito que cada arquitetura tem um subconjunto difícil de modelar, que pode ser usado em linha asm! , e talvez devêssemos nos concentrar nesses subconjuntos e, em seguida, tentar generalizar.

cc @ rust-lang / lang

@eddyb _require_ é uma palavra forte, e eu seria compelido a dizer que não, não somos estritamente obrigados a usar o assembler embutido. Como mencionei antes, nós _podemos_ definir procedimentos em linguagem assembly pura, montá-los separadamente e vinculá-los a nossos programas Rust por meio do FFI.

No entanto, como eu disse antes, não conheço nenhum projeto sério de nível de sistema operacional que faça isso. Isso significaria muitos boiler plate (leia-se: mais chances de cometer um erro), um processo de compilação mais complexo (neste momento, temos a sorte de podermos sair impunes com uma simples invocação cargo e um link e um kernel quase pronto para rodar sai do outro lado; teríamos que invocar o assembler e o link em uma etapa separada), e uma diminuição drástica na capacidade de incorporar coisas, etc; quase certamente haveria um impacto no desempenho.

Coisas como intrínsecos do compilador ajudam em muitos casos, mas para coisas como o conjunto de instruções de supervisão do ISA alvo, particularmente recursos de hardware mais esotéricos (recursos de hipervisor e enclave, por exemplo), geralmente não há intrínsecos e estamos dentro um ambiente no_std. O que há de intrínseco muitas vezes não é suficiente; por exemplo, a convenção de chamada de interrupção x86 parece legal, mas não dá acesso mutável aos registradores de propósito geral em um quadro de trap: suponha que eu pegue uma exceção de instrução indefinida com a intenção de fazer emulação e suponha que a instrução emulada retorne um valor em% rax ou algo assim; a convenção de chamada não me dá uma boa maneira de passar isso de volta para o site de chamada, então tivemos que lançar o nosso próprio. Isso significava escrever meu próprio código de tratamento de exceções no assembler.

Então, para ser honesto, não, nós não _requigimos_ montador embutido, mas é suficientemente útil que seria quase impossível não tê-lo.

@dancrossnyc Estou especificamente curioso sobre como evitar montagem separado, isto é, que tipo de montagem que você precisa em tudo em seu projeto, não importa como você ligá-lo em.

No seu caso, parece ser um subconjunto ISA com privilégios de supervisor / hipervisor / enclave, correto?

muitas vezes não são intrínsecos

É por necessidade, ou seja, as instruções têm requisitos que são excessivamente difíceis ou mesmo impossíveis de manter quando compiladas como chamadas intrínsecas, por exemplo, LLVM?
Ou isso é apenas porque eles são considerados muito especiais para serem úteis para a maioria dos desenvolvedores?

e estamos em um ambiente no_std

Para o registro, os intrínsecos do fornecedor estão em std::arch e core::arch (o primeiro é uma reexportação).

a convenção de chamada de interrupção x86 parece legal, mas não dá acesso mutável aos registradores de propósito geral em um quadro de trap

cc @rkruppe Isso pode ser implementado no LLVM?

@eddyb correto; precisamos do subconjunto supervisor do ISA. Infelizmente, não posso dizer muito mais no momento sobre nosso caso de uso específico.

É por necessidade, ou seja, as instruções têm requisitos que são excessivamente difíceis ou mesmo impossíveis de manter quando compiladas como chamadas intrínsecas, por exemplo, LLVM?
Ou isso é apenas porque eles são considerados muito especiais para serem úteis para a maioria dos desenvolvedores?

Até certo ponto, ambos são verdadeiros, mas, no geral, eu diria que o último é mais relevante aqui. Algumas coisas são específicas da microarquitetura e dependem de configurações específicas do pacote do processador. Seria razoável para um compilador (por exemplo) expor algo como um intrínseco que faz parte do subconjunto de instruções privilegiadas _e_ condicionado a uma versão de processador específica? Sinceramente não sei.

Para o registro, os intrínsecos do fornecedor estão em std :: arch e core :: arch (o primeiro é uma reexportação).

É realmente muito bom saber disso. Obrigado!

Seria razoável para um compilador (por exemplo) expor algo como um intrínseco que faz parte do subconjunto de instruções privilegiadas e condicionado a uma versão de processador específica? Sinceramente não sei.

Nós já fazemos. Por exemplo, as instruções xsave x86 são implementadas e expostas em core::arch , não disponível em todos os processadores, e a maioria deles requer modo privilegiado.

@gnzlbg xsave não tem privilégios; você quis dizer xsaves ?

Dei uma olhada em https://rust-lang-nursery.github.io/stdsimd/x86_64/stdsimd/arch/x86_64/index.html e nas únicas instruções privilegiadas que vi em minha varredura rápida (não fiz uma pesquisa exaustiva) foram xsaves , xsaves64 , xrstors e xrstors64 . Suspeito que sejam intrínsecos porque se enquadram na família XSAVE* e não geram exceções no modo real, e algumas pessoas querem usar o clang / llvm para compilar o código em modo real.

@dancrossnyc sim, alguns desses são os que eu quis dizer (implementamos xsave , xsaves , xsaveopt , ... no módulo xsave : https: //github.com/rust-lang-nursery/stdsimd/blob/master/coresimd/x86/xsave.rs).

Eles estão disponíveis em core , portanto, você pode usá-los para escrever um kernel do sistema operacional para x86. No espaço do usuário, eles são AFAICT inúteis (eles sempre geram uma exceção), mas não temos uma maneira de distinguir isso em core . No entanto, só pudemos expô-los em core e não em std , mas como eles já estão estáveis, o navio partiu. Quem sabe, talvez algum sistema operacional rode tudo no anel 0 algum dia, e você possa usá-los lá ...

@gnzlbg Não sei por que xsaveopt ou xsave levantaria uma exceção no espaço do usuário: xsaves é o único da família definida para gerar uma exceção (#GP se CPL> 0), e então apenas no modo protegido (SDM vol.1 ch. 13; vol.2C ch. 5 XSAVES). xsave e xsaveopt são úteis para implementar, por exemplo, threads de espaço do usuário preventivos, então sua presença como intrínseca realmente faz sentido. Suspeito que o intrínseco de xsaves foi porque alguém acabou de adicionar tudo da família xsave sem perceber o problema de privilégio (isto é, supondo que fosse invocável do espaço do usuário), ou alguém quis chamá-lo do modo real. Este último pode parecer rebuscado, mas sei que as pessoas estão, por exemplo, construindo firmware em modo real com Clang e LLVM.

Não me interpretem mal; a presença de intrínsecos LLVM em core é ótima; se eu nunca tiver que escrever aquela sequência boba de instruções para obter os resultados de rdtscp em um formato útil novamente, ficarei feliz. Mas o conjunto atual de intrínsecos não é um substituto para o montador embutido quando você está escrevendo um kernel ou outro tipo de supervisão bare-metal.

@dancrossnyc quando mencionei xsave estava me referindo a alguns dos intrínsecos que estão disponíveis por trás dos bits CPUID XSAVE, XSAVEOPT, XSAVEC, etc. Alguns desses intrínsecos requerem modo privilegiado.

Seria razoável para um compilador (por exemplo) expor algo como um intrínseco que faz parte do subconjunto de instruções privilegiadas e condicionado a uma versão de processador específica?

Já o fazemos e eles estão disponíveis em Rust estável.

Eu suspeito que o intrínseco para xsaves era porque alguém acabou de adicionar tudo da família xsave sem perceber o problema de privilégio

Eu adicionei esses intrínsecos. Percebemos os problemas de privilégios e decidimos adicioná-los de qualquer maneira porque é perfeitamente normal para um programa que depende de core ser um kernel do sistema operacional que deseja usá-los, e eles são inofensivos no espaço do usuário (como em, se você tente usá-los, seu processo termina).

Mas o conjunto atual de intrínsecos não é um substituto para o montador embutido quando você está escrevendo um kernel ou outro tipo de supervisão bare-metal.

Concordo, é por isso que este problema ainda está aberto;)

@gnzlbg desculpe, eu não pretendo inviabilizar isso xsave et al.

No entanto, pelo que posso dizer, os únicos intrínsecos que requerem execução privilegiada são aqueles relacionados a xsaves e, mesmo assim, nem sempre é privilegiado (novamente, o modo real não se importa). É maravilhoso que eles estejam disponíveis no Rust estável (sério). Os outros podem ser úteis no espaço do usuário e, da mesma forma, acho ótimo que eles estejam lá. No entanto, xsaves e xrstors são uma porção muito, muito pequena do conjunto de instruções privilegiadas e ter adicionado intrínsecos para duas instruções é qualitativamente diferente do que fazer em geral e eu acho que a questão permanece: é apropriado _ em geral_. Considere a instrução VMWRITE das extensões VMX, por exemplo; Eu imagino que um intrínseco faria algo como executar uma instrução e então "retornar" rflags . Isso é uma coisa estranhamente especializada para se ter como algo intrínseco.

Acho que, caso contrário, estamos de acordo aqui.

FWIW, de acordo com a RFC std::arch , atualmente só podemos adicionar intrínsecos a std::arch que os fornecedores expõem em suas APIs. Para o caso de xsave , a Intel os expõe em sua API C , então é por isso que está tudo bem. Se você precisar de qualquer intrínseco do fornecedor que não esteja exposto no momento, abra um problema, se ele requer o modo privilegiado ou não, é irrelevante.

Se o fornecedor não expõe um intrínseco para ele, então std::arch pode não ser o lugar para ele, mas existem muitas alternativas para isso (montagem em linha, conjunto global, chamando C, ...).

Desculpe, entendi que você disse que escreveu os intrínsecos de xsave para significar os intrínsecos da Intel; meus comentários anteriores ainda se aplicam ao motivo pelo qual eu acho que xsaves é um elemento intrínseco (um acidente de um redator de compilador da Intel ou porque alguém o queria em modo real; sinto que o primeiro seria notado muito rapidamente, mas firmware faz coisas estranhas, então o último não me surpreenderia de forma alguma).

De qualquer forma, sim, acho que concordamos fundamentalmente: intrínsecos não são o lugar para tudo, e é por isso que gostaríamos de ver o conjunto! () Movido para estável. Estou muito animado em saber que progresso está sendo feito nesta área, como você disse ontem, e se pudermos gentilmente cutucar @Florob para subir mais perto do topo da pilha,

Alguns detalhes adicionais e casos de uso para asm! :

Ao escrever um sistema operacional, firmware, certos tipos de bibliotecas ou certos outros tipos de código de sistema, você precisa de acesso total à montagem em nível de plataforma. Mesmo se tivéssemos intrínsecos que expusessem todas as instruções em todas as arquiteturas que o Rust suporta (o que não chegamos nem perto de ter), isso ainda não seria o suficiente para algumas das acrobacias que as pessoas costumam fazer com a montagem embutida.

Aqui estão uma pequena fração do que você pode fazer com a montagem embutida e que não pode ser feito facilmente de outras maneiras. Cada um deles é um exemplo do mundo real que vi (ou, em alguns casos, escrito), não um exemplo hipotético.

  • Colete todas as implementações de um determinado padrão de instruções em uma seção ELF separada e, em seguida, no código de carregamento, corrija essa seção no tempo de execução com base nas características do sistema em que você executa.
  • Escreva uma instrução de salto cujo destino seja corrigido no tempo de execução.
  • Emita uma sequência exata de instruções (de forma que você não possa contar com intrínsecos para as instruções individuais), de modo que possa implementar um padrão que trata cuidadosamente as interrupções potenciais no meio.
  • Emita uma instrução, seguida por um salto para o final do bloco asm, seguido por um código de recuperação de falha para um manipulador de falha de hardware para saltar se a instrução gerar uma falha.
  • Emita uma sequência de bytes correspondente a uma instrução que o montador ainda não conhece.
  • Escreva um trecho de código que alterne cuidadosamente para uma pilha diferente e depois chame outra função.
  • Chame rotinas de montagem ou chamadas de sistema que requerem argumentos em registros específicos.

+ 1e6

@eddyb

Ok, vou tentar a abordagem intrínseca e ver onde isso leva. Você provavelmente está certo e essa é a melhor abordagem para o meu caso. Obrigado!

@joshtriplett acertou em cheio ! Esses são os casos de uso exatos que eu tinha em mente.

loop {
   :thumbs_up:
}

Eu adicionaria alguns outros casos de uso:

  • escrever código em modos arquitetônicos estranhos, como chamadas BIOS / EFI e modo real de 16 bits.
  • escrever código com modos de endereçamento estranhos / incomuns (que geralmente aparecem em modo real de 16 bits, bootloaders, etc.)

@ mark-im Com certeza! E generalizando um ponto que tem subcasos em ambas as nossas listas: traduzindo entre convenções de chamada.

Estou encerrando o número 53118 em favor desta questão e copiando o PR aqui para registro. Observe que é de agosto, mas uma breve olhada parece indicar que a situação não mudou:


A seção sobre montagem em linha precisa de uma revisão; em seu estado atual, isso implica que o comportamento e a sintaxe estão ligados a rustc e à linguagem rust em geral. Praticamente toda a documentação é específica para o assembly x86 / x86_64 com o conjunto de ferramentas llvm. Para ser claro, não estou me referindo ao código do assembly em si, que é obviamente específico da plataforma, mas sim à arquitetura geral e ao uso do assembly embutido como um todo.

Eu não encontrei uma fonte confiável para o comportamento do assembly embutido quando se trata de destino ARM, mas por minha experimentação e referenciando a documentação do assembly embutido ARM GCC , os seguintes pontos parecem estar completamente errados:

  • A sintaxe ASM, como ARM / MIPS (e muitos outros CISC?) Usa sintaxe intel-esque com o registrador de destino primeiro. Eu entendi que a documentação significa / implica que asm inline assumiu a sintaxe at & t que foi transpilada para a sintaxe específica da plataforma / compilador real, e que eu deveria apenas substituir os nomes dos registros x86 pelos dos registros ARM apenas.
  • Da mesma forma, a opção intel é inválida, pois causa erros de "diretiva desconhecida" durante a compilação .
  • Adaptando da documentação do assembly embutido ARM GCC (para construir contra thumbv7em-none-eabi com o conjunto de ferramentas arm-none-eabi-* , parece que mesmo algumas suposições básicas sobre o formato do assembly embutido são específicas da plataforma. parece que para ARM o registro de saída (segundo argumento da macro) conta como uma referência de registro, ou seja, $0 refere-se ao primeiro registro de saída e não ao primeiro registro de entrada, como é o caso com as instruções x86 llvm.
  • Ao mesmo tempo, outros recursos específicos do compilador _não_ estão presentes; Não posso usar referências nomeadas para registros, apenas índices (por exemplo, asm("mov %[result],%[value],ror #1":[result] "=r" (y):[value] "r" (x)); é inválido).
  • (Mesmo para destinos x86 / x86_64, o uso de $0 e $2 no exemplo de assembly embutido é muito confuso, pois não explica por que esses números foram escolhidos.)

Acho que o que mais me impressionou foi a declaração final:

A implementação atual do asm! macro é uma ligação direta às expressões assembler inline do LLVM, então certifique-se de verificar sua documentação também para obter mais informações sobre clobbers, restrições, etc.

O que não parece ser universalmente verdadeiro.

Eu entendi que a documentação significa / implica que asm inline assumiu a sintaxe at & t que foi transpilada para a sintaxe específica da plataforma / compilador real, e que eu deveria apenas substituir os nomes dos registros x86 pelos dos registros ARM apenas.

A noção de sintaxe intel vs at & t existe apenas no x86 (embora possa haver outros casos dos quais eu não saiba). É único porque são duas linguagens diferentes que compartilham o mesmo conjunto de mnemônicos para representar o mesmo conjunto de código binário. O ecossistema GNU estabeleceu a sintaxe at & t como o padrão dominante para o mundo x86, razão pela qual é o padrão do ASM inline. Você está enganado quanto ao fato de ser uma ligação direta às expressões assembler embutidas do LLVM que, por sua vez, apenas despejam o texto simples (após as substituições de processamento) no programa textual assembly. Nada disso é exclusivo (ou mesmo relevante) para ou sobre os asm!() , pois é inteiramente específico da plataforma e completamente sem sentido além do mundo x86.

Da mesma forma, a opção intel é inválida, pois causa erros de "diretiva desconhecida" durante a compilação.

Esta é uma consequência direta da inserção "burra" / simples de texto plano que descrevi acima. Como a mensagem de erro indica, a diretiva .intel_syntax não é compatível. Esta é uma solução alternativa antiga e bem conhecida para usar asm inline no estilo intel com GCC (que emite o estilo att): alguém simplesmente escreveria .intel_syntax no início do bloco asm inline e, em seguida, escreveria algum intel- estilize asm e finalmente termine com .att_syntax para colocar o montador de volta no modo att para que ele processe corretamente o (a seguir) código gerado pelo compilador mais uma vez. É um hack sujo e lembro que pelo menos a implementação do LLVM teve algumas peculiaridades por muito tempo, então parece que você está vendo este erro porque ele foi finalmente removido. Infelizmente, o único curso de ação correto aqui é remover a opção "intel" do rustc.

parece que mesmo algumas suposições básicas sobre o formato do assembly inline são específicas da plataforma

Sua observação está totalmente correta, cada plataforma apresenta seu próprio formato binário e sua própria linguagem assembly. Eles são completamente independentes e (em sua maioria) não processados ​​pelo compilador - que é o ponto principal da programação no assembler bruto!

Não posso usar referências nomeadas para registros, apenas índices

Infelizmente, há uma grande incompatibilidade entre a implementação do LLVM inline asm que o rustc expõe e a implementação do GCC (que emula o clang). Sem uma decisão sobre como seguir em frente com asm!() há pouca motivação em melhorar isso - além disso, descrevi as principais opções há muito tempo, todas elas têm desvantagens claras. Já que isso não parece ser uma prioridade, você provavelmente vai ficar preso com os asm!() por alguns anos, pelo menos. Existem soluções alternativas decentes:

  • confie no otimizador para produzir o código ideal (com um pequeno empurrão, geralmente você pode obter exatamente o que deseja, sem nunca escrever um assembly bruto sozinho)
  • use o intrínseco, outra solução bastante elegante que é melhor do que o conjunto em linha em quase todos os sentidos (a menos que você precise de controle exato sobre a seleção e programação de instruções)
  • invoque a caixa cc de build.rs para vincular um objeto C com conjunto embutido

    • Basicamente, basta invocar qualquer assembler de build.rs , usar um compilador C pode parecer um exagero, mas evita o incômodo de integração com o sistema build.rs

Essas soluções alternativas se aplicam a todos, exceto a um pequeno conjunto de casos extremos muito específicos. Se você acertar um deles (felizmente ainda não), você está sem sorte.

Concordo que a documentação é bastante sem brilho, mas é boa o suficiente para qualquer pessoa familiarizada com asm inline. Se não estiver, provavelmente não deveria usá-lo . Não me interpretem mal - você definitivamente deve se sentir livre para experimentar e aprender, mas como asm!() é instável e negligenciado e como existem soluções realmente boas, eu desaconselho fortemente seu uso em qualquer projeto sério, se possível .

invocar o cc crate de build.rs para vincular um objeto C com asm em linha

Você também pode invocar a caixa cc de build.rs para construir arquivos de montagem simples, que fornecem a quantidade máxima de controle. Eu recomendo fortemente fazer exatamente isso, caso as duas "soluções alternativas" acima não funcionem para o seu caso de uso.

@ main-- escreveu:

Essas soluções alternativas se aplicam a todos, exceto a um pequeno conjunto de casos extremos muito específicos. Se você acertar um deles (felizmente ainda não), você está sem sorte.

Quer dizer, não totalmente sem sorte. Você apenas tem que usar inline asm Rust. Tenho um caso extremo que nenhuma das soluções alternativas listadas cobre aqui . Como você disse, se você está familiarizado com o processo de outros compiladores, geralmente não há problema.

(Eu tenho outro caso de uso: gostaria de ensinar arquitetura de computador de programação de sistemas e outras coisas usando Rust em vez de C algum dia. Não ter assembly embutido tornaria isso muito mais estranho.)

Gostaria que fizéssemos da montagem em linha uma prioridade em Rust e a estabilizássemos mais cedo ou mais tarde. Talvez essa devesse ser uma meta do Rust para 2019. Estou bem com qualquer uma das soluções que você listou em seu agradável comentário anterior : Eu poderia viver com os problemas de qualquer uma delas. Ser capaz de embutir código assembly é para mim um pré-requisito para escrever Rust em vez de C em qualquer lugar: eu realmente preciso que ele seja estável.

Gostaria que fizéssemos da montagem em linha uma prioridade em Rust e a estabilizássemos mais cedo ou mais tarde. Talvez essa devesse ser uma meta do Rust para 2019.

Escreva uma postagem do blog Rust 2019 e expresse essa preocupação. Acho que se um número suficiente de nós fizer isso, podemos influenciar o roteiro.

Para esclarecer meu comentário acima - o problema é que a documentação não explica o quão "profundamente" o conteúdo da macro asm!(..) é analisado / interagido. Estou familiarizado com o x86 e o ​​assembly MIPS / ARM, mas presumi que o llvm tivesse seu próprio formato de linguagem assembly. Eu usei o assembly inline para x86 antes, mas não estava claro até que ponto a bastardização de asm para brige C e ASM foi. Minha suposição (agora invalidada) com base nas palavras na seção de montagem inline enferrujada era que o LLVM tinha seu próprio formato ASM que foi construído para imitar o assembly x86 nos modos at & t ou intel e necessariamente se parecia com os exemplos x86 mostrados.

(O que me ajudou foi estudar a saída macro expandida, o que esclareceu o que estava acontecendo)

Acho que precisa haver menos abstração nessa página. Deixe mais claro o que é analisado pelo LLVM e o que é interpretado como ASM diretamente. Quais partes são específicas para a ferrugem, quais partes são específicas para o hardware em que você está trabalhando e quais partes pertencem à cola que os mantém unidos.

invoque a caixa cc de build.rs para vincular um objeto C ao conjunto embutido

O progresso recente no LTO entre linguagens me faz pensar se algumas das desvantagens dessa avenida podem ser reduzidas, efetivamente alinhando esse "blob de assembly externo". ( provavelmente não )

invoque a caixa cc de build.rs para vincular um objeto C ao conjunto embutido

O progresso recente no LTO entre linguagens me faz pensar se algumas das desvantagens dessa avenida podem ser reduzidas, efetivamente alinhando esse "blob de assembly externo".

Mesmo que funcione, não quero escrever minha montagem embutida em C. Quero escrevê-la em Rust. :-)

Não quero escrever minha montagem embutida em C.

Você pode compilar e vincular .s e .S arquivos diretamente (veja por exemplo esta caixa ), que em meu livro estão longe o suficiente de C. :)

se algumas das desvantagens desta avenida podem ser reduzidas

Acredito que isso não seja viável atualmente, pois o LTO entre linguagens depende de ter o LLVM IR e o assembly não geraria isso.

Acredito que isso não seja viável atualmente, pois o LTO entre linguagens depende de ter o LLVM IR e o assembly não geraria isso.

Você pode colocar a montagem na montagem de nível de módulo em módulos LLVM IR.

Alguém sabe qual é a proposta / status atual mais recente? Já que o tema do ano é "maturidade e finalização do que começamos", parece uma ótima oportunidade para finalmente finalizar asm .

Planos vagos para uma nova sintaxe (a ser estabilizada) foram discutidos em fevereiro passado: https://paper.dropbox.com/doc/FFI-5NmXV30TGiSsr9dIxpqpq

De acordo com essas notas, @joshtriplett e @Amanieu se inscreveram para escrever um RFC.

Qual é o status da nova sintaxe?

Precisa ser RFC'ed e implementado todas as noites

ping @joshtriplett @Amanieu Deixe-me saber se eu posso ajudar a mover as coisas aqui! Entrarei em contato em breve.

@cramertj AFAICT qualquer um pode seguir em frente, está desbloqueado e esperando que alguém entre e coloque o trabalho. Há um pré-RFC esboçando o design geral e as próximas etapas podem ser implementá-lo e ver se ele realmente funciona, seja como uma macro proc, em uma bifurcação ou como um recurso instável diferente.

Provavelmente, alguém poderia tentar transformar esse pré-RFC em um RFC adequado e enviá-lo, mas duvido que sem uma implementação tal RFC possa ser convincente.


EDITAR: para ser claro, por convencer quero dizer especificamente partes do pré-RFC como esta:

adicionalmente, mapeamentos para classes de registro são adicionados conforme apropriado (cf. llvm-constraint 6)

onde há dezenas de classes de registro específicas de arco no lang-ref. Um RFC não pode simplesmente dispensar tudo isso e certificar-se de que todos funcionem como deveriam, ou sejam significativos, ou sejam "estáveis" o suficiente no LLVM para serem expostos a partir daqui, etc. se beneficiaria de uma implementação que pode ser apenas experimente isso.

O assembly em linha RISC-V é suportado aqui com #![feature(asm)] ?

Tanto quanto é do meu conhecimento, toda a montagem em plataformas suportadas é suportada; trata-se basicamente de acesso bruto ao suporte asm do compilador llvm.

Sim, RISC-V é compatível. As classes de restrição de entrada / saída / clobber específicas da arquitetura são documentadas no langref do LLVM .

No entanto, há uma advertência - se você precisar restringir os registros individuais nas restrições de entrada / saída / clobber, você deve usar os nomes dos registros arquitetônicos (x0-x31, f0-f31), não os nomes ABI. No próprio fragmento de montagem, você pode usar qualquer tipo de nome de registro.

Como alguém novo para esses conceitos, posso apenas dizer ... toda essa discussão parece _silly_. Como é que uma linguagem (assembly) que deveria ser um mapeamento 1 para 1 com seu código de máquina causa tanta dor de cabeça?

Estou muito confuso:

  • Se você está escrevendo comom, não deveria ser reescrito (por um humano com #[cfg(...)] ) para cada arquitetura _e backend_ que você está tentando oferecer?
  • Isso significa que a questão da "sintaxe" é discutível ... basta usar a sintaxe para essa arquitetura e o backend que o compilador está usando.
  • O Rust precisaria apenas de funções std inseguras para ser capaz de colocar bytes nos registradores corretos e empurrar / pop para a pilha para qualquer arquitetura que esteja sendo compilada - novamente, isso pode ter que ser reescrito para cada arquitetura e talvez até mesmo para cada back-end.

Eu entendo que a compatibilidade com versões anteriores é um problema, mas com o grande número de bugs e o fato de que isso nunca foi estabilizado, talvez seja melhor apenas repassá-lo para o backend. Rust não deveria tentar consertar os erros de sintaxe do LLVM, do gcc ou de qualquer outra pessoa. Rust está no negócio de emitir código de máquina para a arquitetura e o compilador que tem como objetivo ... e asm já é basicamente esse código!

O motivo de não haver progresso aqui é que ninguém está investindo tempo para corrigir esse problema. Esse não é um bom motivo para estabilizar um recurso.

Enquanto lia este tópico, tive uma ideia e tive que postá-la. Desculpe se estou respondendo a uma postagem antiga, mas achei que valeu a pena:

@ main-- disse:

Ambas as otimizações certamente geram melhor rendimento (especialmente em face do hyperthreading), mas não a redução de latência que eu esperava alcançar. Eu acabei caindo no nasm para aquele experimento, mas ter que reescrever o código do intrínseco para o asm simples foi apenas um atrito desnecessário. Claro que quero que o otimizador lide com coisas como seleção de instruções ou dobramento constante ao usar alguma API vetorial de alto nível. Mas quando decidi explicitamente quais instruções usar, realmente não quero que o compilador mexa nisso. A única alternativa é o conjunto em linha.

Talvez em vez do conjunto embutido, o que realmente precisamos aqui são atributos de função para LLVM que informam ao otimizador: "otimize isso para o rendimento", "otimize isso para latência", "otimize isso para o tamanho binário". Eu sei que esta solução é upstream, mas ela não só resolveria seu problema particular automaticamente (fornecendo a menor latência, mas de outra forma a implementação isomórfica do algoritmo), também permitiria aos programadores Rust ter um controle mais refinado sobre as características de desempenho que importa para eles.

@ felix91gr Isso não resolve os casos de uso que requerem a emissão de uma seqüência exata de instruções, por exemplo, manipuladores de interrupção.

@ mark-im, claro que não. É por isso que coloquei uma citação literal! 🙂

Meu ponto é que mesmo que você possa resolver o "compilador otimiza de uma maneira oposta ao que eu preciso" (que é clássico no caso deles: latência vs taxa de transferência) usando recursos de conjunto em linha, talvez (e definitivamente) esse caso de uso seria ser servido melhor por um controle mais refinado de otimizações :)

À luz das mudanças futuras na montagem embutida, a maior parte da discussão nesta edição não é mais relevante. Como tal, encerrarei este problema em favor de dois problemas de rastreamento separados para cada tipo de montagem em linha que temos:

  • Rastreamento de problema para montagem embutida no estilo LLVM ( llvm_asm ) # 70173
  • Rastreamento de problema para montagem em linha ( asm! ) # 72016
Esta página foi útil?
0 / 5 - 0 avaliações