Rust: A otimização do loop LLVM pode fazer com que programas seguros travem

Criado em 29 set. 2015  ·  97Comentários  ·  Fonte: rust-lang/rust

O seguinte snippet falha quando compilado no modo de lançamento no estável atual, beta e noturno:

enum Null {}

fn foo() -> Null { loop { } }

fn create_null() -> Null {
    let n = foo();

    let mut i = 0;
    while i < 100 { i += 1; }
    return n;
}

fn use_null(n: Null) -> ! {
    match n { }
}


fn main() {
    use_null(create_null());
}

https://play.rust-lang.org/?gist=1f99432e4f2dccdf7d7e&version=stable

Isso se baseia no seguinte exemplo de LLVM removendo um loop do qual fui informado: https://github.com/simnalamburt/snippets/blob/12e73f45f3/rust/infinite.rs.
O que parece acontecer é que, uma vez que C permite que o LLVM remova loops infinitos que não têm efeito colateral, acabamos executando um match que deve ser acionado.

A-LLVM C-bug E-medium I-needs-decision I-unsound 💥 P-medium T-compiler WG-embedded

Comentários muito úteis

Caso alguém queira jogar golfe de código de caso de teste:

pub fn main() {
   (|| loop {})()
}

Com o sinalizador -Z insert-sideeffect rustc, adicionado por @sfanxiang em https://github.com/rust-lang/rust/pull/59546, ele continua em loop :)

antes:

main:
  ud2

depois de:

main:
.LBB0_1:
  jmp .LBB0_1

Todos 97 comentários

O LLVM IR do código otimizado é

; Function Attrs: noreturn nounwind readnone uwtable
define internal void @_ZN4main20h5ec738167109b800UaaE() unnamed_addr #0 {
entry-block:
  unreachable
}

Esse tipo de otimização quebra a suposição principal que normalmente deveria ser mantida em tipos desabitados: deveria ser impossível ter um valor desse tipo.
rust-lang / rfcs # 1216 se propõe a lidar explicitamente com tais tipos no Rust. Pode ser eficaz para garantir que o LLVM nunca precise tratá-los e para injetar o código apropriado para garantir a divergência quando necessário (IIUIC isso pode ser alcançado com atributos apropriados ou chamadas intrínsecas).
Este tópico também foi discutido recentemente na lista de e-mails do LLVM: http://lists.llvm.org/pipermail/llvm-dev/2015-July/088095.html

triagem: I-nomeado

Parece ruim! Se o LLVM não tem uma maneira de dizer "sim, este loop é realmente infinito", entretanto, podemos apenas ter que sentar e esperar que a discussão inicial seja resolvida.

Uma maneira de evitar que os loops infinitos sejam otimizados é adicionar unsafe {asm!("" :::: "volatile")} dentro deles. Isso é semelhante ao llvm.noop.sideeffect intrínseco que foi proposto na lista de discussão LLVM, mas pode impedir algumas otimizações.
A fim de evitar a perda de desempenho e ainda garantir que funções / loops divergentes não sejam totalmente otimizados, acredito que deve ser suficiente inserir um loop não otimizável vazio (ou seja, loop { unsafe { asm!("" :::: "volatile") } } ) se os valores desabitados estiverem em escopo.
Se o LLVM otimizar o código que deve divergir a ponto de não divergir mais, tais loops garantirão que o fluxo de controle ainda não possa prosseguir.
No caso de "sorte" em que o LLVM é incapaz de otimizar o código divergente, tal loop será removido pelo DCE.

Isso está relacionado a # 18785? Essa é sobre recursão infinita para ser UB, mas parece que a causa fundamental pode ser semelhante: o LLVM não considera não interromper como um efeito colateral, então se uma função não tem efeitos colaterais além de não interromper, é feliz em otimizar embora.

@geofft

É o mesmo problema.

Sim, parece que é o mesmo. Mais adiante nessa questão, eles mostram como obter undef , a partir do qual presumo que não seja difícil fazer um programa (aparentemente seguro) travar.

: +1:

Crash ou, possivelmente, pior ainda, sangramento do coração https://play.rust-lang.org/?gist=15a325a795244192bdce&version=stable

Então, estive pensando quanto tempo até alguém relatar isso. :) Na minha opinião, a melhor solução seria, claro, se pudéssemos dizer ao LLVM para não ser tão agressivo com loops potencialmente infinitos. Caso contrário, a única coisa que acho que podemos fazer é fazer uma análise conservadora na própria Rust que determine se:

  1. o loop vai terminar OU
  2. o loop terá efeitos colaterais (operações de E / S etc, esqueci exatamente como isso é definido em C)

Qualquer uma dessas coisas deve ser suficiente para evitar um comportamento indefinido.

triagem: P-médio

Gostaríamos de ver o que o LLVM fará antes de investirmos muito esforço do nosso lado, e isso parece relativamente improvável de causar problemas na prática (embora eu tenha acertado pessoalmente durante o desenvolvimento do compilador também). Não há problemas de incomatibilidade para trás com que se preocupar.

Citando a discussão da lista de discussão LLVM:

 The implementation may assume that any thread will eventually do one of the following:
   - terminate
   - make a call to a library I/O function
   - access or modify a volatile object, or
   - perform a synchronization operation or an atomic operation

 [Note: This is intended to allow compiler transformations such as removal of empty loops, even
  when termination cannot be proven. — end note ]

@dotdash O trecho que você está citando vem da especificação C ++; é basicamente a resposta para "como [ter efeitos colaterais] é definido em C" (também confirmado pelo comitê padrão: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528 .htm).

Em relação a qual é o comportamento esperado do LLVM IR existe alguma confusão. https://llvm.org/bugs/show_bug.cgi?id=24078 mostra que parece não haver uma especificação precisa e explícita da semântica de loops infinitos no LLVM IR. Ele se alinha com a semântica do C ++, provavelmente por razões históricas e por conveniência (eu só consegui rastrear https://groups.google.com/forum/#!topic/llvm-dev/j2vlIECKkdE que aparentemente se refere a um tempo quando os loops infinitos não foram otimizados, algum tempo antes das especificações C / C ++ serem atualizadas para permitir isso).

A partir do thread, fica claro que há o desejo de otimizar o código C ++ da forma mais eficaz possível (ou seja, levando em consideração a oportunidade de remover loops infinitos), mas no mesmo thread vários desenvolvedores (incluindo alguns que contribuem ativamente para o LLVM) têm mostrou interesse na capacidade de preservar loops infinitos, pois eles são necessários para outras linguagens.

@ ranma42 Estou ciente disso, acabei de citar isso para referência, porque uma possibilidade de contornar isso seria detectar esses loops em ferrugem e adicionar um dos itens acima para impedir o LLVM de realizar essa otimização.

Este é um problema de solidez? Em caso afirmativo, devemos marcá-lo como tal.

Sim, seguindo o exemplo de @ ranma42 , esta maneira mostra como ele vence prontamente as verificações de limites de array. link playground

@bluss

A política é que problemas de código errado que também são problemas de integridade (ou seja, a maioria deles) devem ser marcados I-wrong .

Portanto, apenas para recapitular a discussão anterior, há realmente duas opções aqui que posso ver:

  • Aguarde até que o LLVM forneça uma solução.
  • Apresente instruções asm autônomas sempre que houver um loop infinito ou recursão infinita (# 18785).

O último é meio ruim porque pode inibir a otimização, então gostaríamos de fazê-lo com moderação - basicamente onde quer que não possamos provar a rescisão por nós mesmos. Você também pode criar imagens vinculando-o um pouco mais à forma como o LLVM é otimizado - ou seja, apresentando apenas se pudermos detectar um cenário que o LLVM possa considerar ser um loop / recursão infinito - mas que (a) exigiria o rastreamento do LLVM e (b ) requerem um conhecimento mais profundo do que eu, pelo menos, possuo.

Aguarde até que o LLVM forneça uma solução.

Qual é o bug do LLVM rastreando esse problema?

observação lateral: while true {} exibe esse comportamento . Talvez o lint deva ser atualizado para erro por padrão e obter uma nota informando que isso atualmente pode exibir um comportamento indefinido.

Além disso, observe que isso é inválido para C. LLVM fazer este argumento significa que há um bug no clang.

void foo() { while (1) { } }

void create_null() {
        foo();

        int i = 0;
        while (i < 100) { i += 1; }
}

__attribute__((noreturn))
void use_null() {
        __builtin_unreachable();
}


int main() {
        create_null();
        use_null();
}

Isso trava com otimizações; este é um comportamento inválido no padrão C11:

An iteration statement whose controlling expression is not a constant
expression, [note 156] that performs no  input/output  operations,
does  not  access  volatile  objects,  and  performs  no synchronization or
atomic operations in its body, controlling expression, or (in the case of
a for statement) its expression-3, may be   assumed   by   the
implementation to terminate. [note 157]

156: An omitted controlling expression is replaced by a nonzero constant,
     which is a constant expression.
157: This  is  intended  to  allow  compiler  transformations  such  as
     removal  of  empty  loops  even  when termination cannot be proven. 

Observe que "cuja expressão de controle não é uma expressão constante" - while (1) { } , 1 é uma expressão constante e, portanto, não pode ser removida .

A remoção do loop é um passo de otimização que poderíamos simplesmente remover?

@ubsan

Você encontrou um relatório de bug para isso no bugzilla do LLVM ou preencheu um? Parece que em C ++ os loops infinitos que _podem_ nunca terminar são comportamentos indefinidos, mas em C eles são comportamentos definidos (ou podem ser removidos com segurança em alguns casos, ou não em outros).

@gnzlbg Estou registrando um bug agora.

https://llvm.org/bugs/show_bug.cgi?id=31217

Repetindo # 42009: este bug pode, em algumas circunstâncias, causar a emissão de uma função chamada externamente que não contém nenhuma instrução de máquina. Isso nunca deveria acontecer. Se o LLVM deduzir que pub fn nunca pode ser chamado pelo código correto, ele deve emitir pelo menos uma instrução trap como o corpo dessa função.

O bug do LLVM para isso é https://bugs.llvm.org/show_bug.cgi?id=965 (inaugurado em 2006).

@zackw LLVM tem um sinalizador para isso: TrapUnreachable . Não testei isso, mas parece que adicionar Options.TrapUnreachable = true; a LLVMRustCreateTargetMachine deve atender à sua preocupação. É provável que isso tenha um custo baixo o suficiente para ser feito por padrão, embora eu não tenha feito nenhuma medição.

@oli-obk Infelizmente, não é apenas uma passagem de exclusão de loop. O problema surge de suposições gerais, por exemplo: (a) ramos não têm efeitos colaterais, (b) funções que não contêm instruções com efeitos colaterais não têm efeitos colaterais e (c) chamadas para funções sem efeitos colaterais podem ser movidas ou excluído.

Parece que há um patch: https://reviews.llvm.org/D38336

@sunfishcode , parece que seu patch do LLVM em https://reviews.llvm.org/D38336 foi "aceito" em 3 de outubro, você pode dar uma atualização sobre o que isso significa em relação ao processo de lançamento do LLVM? Qual é o próximo passo além da aceitação, e você tem uma ideia de qual versão futura do LLVM conterá esse patch?

Conversei com algumas pessoas offline que sugeriram que tivéssemos um tópico llvmdev. O tópico está aqui:

http://lists.llvm.org/pipermail/llvm-dev/2017-October/118558.html

Agora está concluído e, como resultado, preciso fazer alterações adicionais. Acho que as mudanças serão boas, embora me levem um pouco mais de tempo para fazer.

Obrigado pela atualização e muito obrigado por seus esforços!

Observe que https://reviews.llvm.org/rL317729 pousou no LLVM. Este patch está planejado para ter um patch de acompanhamento que faz com que os loops infinitos exibam um comportamento definido por padrão, então AFAICT tudo o que precisamos fazer é esperar e, eventualmente, isso será resolvido para nós.

@zackw Agora criei # 45920 para corrigir o problema de funções que não contêm código.

@bstrie Sim, a primeira etapa foi realizada e estou trabalhando na segunda etapa para fazer o LLVM fornecer um comportamento definido de loops infinitos por padrão. É uma mudança complexa e ainda não sei quanto tempo vai demorar para terminar, mas postarei atualizações aqui.

@jsgf Ainda repro. Você selecionou o modo Release?

@kennytm Woops,

Observe que https://reviews.llvm.org/rL317729 pousou no LLVM. Este patch está planejado para ter um patch de acompanhamento que faz com que os loops infinitos exibam um comportamento definido por padrão, então AFAICT tudo o que precisamos fazer é esperar e, eventualmente, isso será resolvido para nós.

Já se passaram vários meses desde este comentário. Alguém sabe se o patch de acompanhamento aconteceu ou ainda vai acontecer?

Alternativamente, parece que o intrínseco llvm.sideeffect existe na versão LLVM que estamos usando: poderíamos consertar isso nós mesmos traduzindo loops infinitos de Rust em loops LLVM que contêm o efeito colateral intrínseco?

Como visto em https://github.com/rust-lang/rust/issues/38136 e https://github.com/rust-lang/rust/issues/54214 , isso é especialmente ruim com os próximos panic_implementation , como uma implementação lógica dele será loop {} , e isso faria todas as ocorrências de panic! UB sem qualquer código unsafe . O que ... talvez seja o pior que poderia acontecer.

Acabei de encontrar esse problema sob outra luz. Aqui está um exemplo:

pub struct Container<'f> {
    string: &'f str,
    num: usize,
}

impl<'f> From<&'f str> for Container<'f> {
    #[inline(always)]
    fn from(string: &'f str) -> Container<'f> {
        Container::from(string)
    }
}

fn main() {
    let x = Container::from("hello");
    println!("{} {}", x.string, x.num);

    let y = Container::from("hi");
    println!("{} {}", y.string, y.num);

    let z = Container::from("hello");
    println!("{} {}", z.string, z.num);
}

Este exemplo segmenta confiavelmente em stable, beta e nightly e mostra como é fácil construir valores não inicializados de qualquer tipo. Aqui está no playground .

@SergioBenitez esse programa não faz segfault, ele termina com um estouro de pilha (você precisa executá-lo no modo de depuração). Este é o comportamento correto, uma vez que seu programa apenas recorre infinitamente, exigindo uma quantidade infinita de espaço de pilha, que em algum ponto excederá o espaço de pilha disponível. Exemplo de trabalho mínimo .

Em versões de lançamento, o LLVM pode assumir que você não tem recursão infinita e otimiza isso ( mwe ). Isso não tem nada a ver com loops AFAICT, mas sim com https://stackoverflow.com/a/5905171/1422197

@gnzlbg Desculpe, mas você não está correto.

O programa falha em segfa no modo de liberação. Esse é o ponto inteiro; que uma otimização resulta em comportamento doentio - que LLVM e a semântica de Rust não estão de acordo aqui - que posso escrever e compilar um programa Rust seguro com rustc que me permite usar memória não inicializada, inspecionar memória arbitrária e lançar arbitrariamente entre tipos, violando a semântica da linguagem. Esse é o mesmo ponto sendo ilustrado neste tópico. Observe que o programa original também não gera segfault no modo de depuração.

Você também parece estar propondo que há uma _diferente_ otimização não-loop ocorrendo aqui. Isso é improvável, embora amplamente irrelevante, embora possa justificar um problema separado, se for o caso. Meu palpite é que o LLVM está percebendo a recursão da cauda, ​​tratando-a como um loop infinito e otimizando-a, novamente, exatamente do que se trata esse problema.

@gnzlbg Bem, mudando ligeiramente seu mwe de otimização de recursão infinita ( aqui ), ele gera um valor não inicializado de NonZeroUsize (que acaba sendo ... 0, portanto, um valor inválido).

E é isso que @SergioBenitez também fez com o exemplo deles, exceto que é com ponteiros, e assim gera um segfault.

Estamos de acordo que o programa @SergioBenitez tem um estouro de pilha tanto na depuração quanto na liberação?

Nesse caso, não consigo encontrar nenhum loop s no exemplo de @SergioBenitez , portanto, não sei como esse problema se aplicaria a ele ( loop s infinitos). Se eu estiver errado, indique-me loop no seu exemplo.

Conforme mencionado, o LLVM assume que a recursão infinita não pode acontecer (assume que todos os threads terminam eventualmente), mas isso seria um problema diferente deste.

Eu não inspecionei as otimizações que o LLVM faz ou o código gerado para nenhum dos programas, mas observe que um segfault não é problemático, se o segfault for tudo o que acontece. Em particular, estouros de pilha que são detectados (por empilhamento + uma página de proteção não mapeada após o final da pilha) e não causam problemas de segurança de memória também aparecem como segfaults. Claro, segfaults também podem indicar corrupção de memória ou gravações / leituras selvagens ou outros problemas de integridade.

@rkruppe Meu programa

@gnzlbg O programa _não_ overflow de pilha no modo de liberação. No modo de liberação, o programa não faz chamadas de função; a pilha é empurrada para um número finito de vezes, puramente para alocar locais.

O programa não acumula estouro no modo de liberação.

Então? A única coisa que importa é que o programa de exemplo, que é basicamente fn foo() { foo() } , tem recursão infinita, o que não é permitido pelo LLVM.

A única coisa que importa é que o programa de exemplo, que é basicamente fn foo () {foo ()}, tem recursão infinita, o que não é permitido pelo LLVM.

Não sei por que você está dizendo que isso resolve alguma coisa. LLVM considerando recursão infinita e loops UB e otimizando de acordo, embora seja seguro no Rust, é o ponto principal de todo este problema!

Autor de https://reviews.llvm.org/rL317729 aqui, confirmando que ainda não implementei o patch de acompanhamento.

Você pode inserir @llvm.sideeffect chamadas hoje para garantir que os loops não sejam otimizados. Isso pode desabilitar algumas otimizações, mas em teoria não muitas, uma vez que as principais otimizações foram ensinadas como entendê-lo. Se alguém colocar @llvm.sideeffect chamadas em todos os loops ou coisas que podem se transformar em loops (recursão, desenrolamento, asm em linha , outros?), Isso é teoricamente suficiente para corrigir o problema aqui.

Obviamente, seria melhor ter o segundo patch no lugar, para que não seja necessário fazer isso. Não sei quando voltarei a implementar isso.

Há uma pequena diferença, mas não tenho certeza se é material ou não.

Recursão

#[allow(unconditional_recursion)]
#[inline(never)]
pub fn via_recursion<T>() -> T {
    via_recursion()
}

fn main() {
    let a: String = via_recursion();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* <strong i="9">@rust_eh_personality</strong> {
_ZN4core3ptr13drop_in_place17h95538e539a6968d0E.exit:
  ret void
}

Ciclo

#[inline(never)]
pub fn via_loop<T>() -> T {
    loop {}
}

fn main() {
    let b: String = via_loop();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 {
start:
  unreachable
}

Meta

Rust 1.29.1, compilando no modo de lançamento, visualizando o LLVM IR.

Não acho que possamos, em geral, detectar recursão (objetos de trait, C FFI, etc.), então teríamos que usar llvm.sideeffect em praticamente todos os sites de chamada, a menos que possamos provar que a chamada o site não recursiva. Provar que não há recursões para os casos em que pode ser provado requer análise interprocedural, exceto para os programas mais triviais como fn main() { main() } . Pode ser bom saber qual é o impacto da implementação dessa correção e se existem soluções alternativas para esse problema.

@gnzlbg Isso é verdade, embora você possa colocar @ llvm.sideeffects nas entradas de funções, em vez de nos sites de chamada.

Estranhamente, não consigo reproduzir o SEGFAULT no caso de teste @SergioBenitez 'localmente.

Além disso, para um estouro de pilha, não deveria haver uma mensagem de erro diferente? Achei que tínhamos algum código para imprimir "A pilha transbordou" ou então?

@RalfJung você tentou no modo de depuração? (Posso reproduzir com segurança o estouro de pilha no modo de depuração em minhas máquinas e no playground, então talvez você precise corrigir um bug se para você não for esse o caso). Em --release você não terá um estouro de pilha porque todo o código foi otimizado incorretamente.


@sunfishcode

Isso é verdade, embora você possa colocar @ llvm.sideeffects nas entradas de funções, em vez de nos sites de chamada.

É difícil dizer qual seria o melhor caminho a seguir sem saber exatamente quais otimizações llvm.sideeffects evitam. Vale a pena tentar gerar o mínimo de @llvm.sideeffects possível? Caso contrário, colocá-lo em todas as chamadas de função pode ser a coisa mais simples de fazer. Caso contrário, IIUC, se @llvm.sideeffect é necessário depende do que o site de chamada faz:

trait Foo {
    fn foo(&self) { self.bar() }
    fn bar(&self);
}

struct A;
impl Foo for A {
    fn bar(&self) {} // not recursive
}
struct B;
impl Foo for B {
    fn bar(&self) { self.foo() } // recursive
}

fn main() {
    let a = A;
    a.bar(); // Ok - no @llvm.sideeffect needed anywhere
    let b = B;
    b.bar(); // We need @llvm.sideeffect on this call site
    let c: &[&dyn Foo] = &[&a, &b];
    for i in c {
        i.bar(); // We need @lvm.sideeffect here too
    }
}

AFAICT, temos que colocar @llvm.sideeffect dentro das funções para evitar que sejam removidas, então mesmo que essas "otimizações" valham a pena, não acho que sejam fáceis de fazer com o modelo atual. Mesmo se fossem, essas otimizações dependeriam da capacidade de provar que não há recursão.

você tentou no modo de depuração? (Posso reproduzir com segurança o estouro de pilha no modo de depuração em minhas máquinas e no playground, então talvez você precise corrigir um bug se para você este não for o caso)

Claro, mas no modo de depuração o LLVM não faz as otimizações de loop, então não há problema.

Se a pilha do programa transbordar no modo de depuração, isso não deve dar licença LLVM para criar UB. O problema é descobrir se o programa final tem UB, e olhando para o IR, não consigo dizer. Segfala, mas não sei por quê. Mas parece um bug para mim "otimizar" um programa stackoverflowing em um que segfaults.

Se a pilha do programa transbordar no modo de depuração, isso não deve dar licença LLVM para criar UB.

Mas parece um bug para mim "otimizar" um programa stackoverflowing em um que segfaults.

Em C, presume-se que um thread de execução termine, execute acessos de memória volátil, E / S ou uma operação atômica de sincronização. Seria surpreendente para mim se o LLVM-IR não tivesse evoluído para ter a mesma semântica por acidente ou por design.

O código Rust contém um thread de execução que nunca termina e não realiza nenhuma das operações necessárias para que isso não seja UB em C. Eu suspeito que geramos o mesmo LLVM-IR que um programa C com comportamento indefinido geraria , então não acho surpreendente que o LLVM esteja otimizando este programa Rust.

Segfala, mas não sei por quê.

O LLVM remove a recursão infinita, então como @SergioBenitez mencionado acima, o programa então prossegue para:

uma referência a um local de memória aleatório foi permitida a ser construída, e a referência foi lida posteriormente.

A parte do programa que faz isso é esta:

let x = Container::from("hello");  // invalid reference created here
println!("{} {}", x.string, x.num);  // invalid reference dereferenced here

onde Container::from inicia uma recursão infinita, que o LLVM conclui que nunca pode acontecer, e substitui por algum valor aleatório, que então é desreferenciado. Você pode ver uma das muitas maneiras em que isso é mal otimizado aqui: https://rust.godbolt.org/z/P7Snex No playground (https://play.rust-lang.org/?gist=f00d41cc189f9f6897d429350f3781ec&version=stable&mode = release & edition = 2015) obtém-se um pânico diferente no lançamento de compilações de depuração devido a essa otimização, mas UB é UB é UB.

O código Rust contém um thread de execução que nunca termina e não realiza nenhuma das operações necessárias para que isso não seja UB em C. Eu suspeito que geramos o mesmo LLVM-IR que um programa C com comportamento indefinido geraria , então não acho surpreendente que o LLVM esteja otimizando este programa Rust.

Fiquei com a impressão de que você argumentou acima que este não é o mesmo bug que o problema do loop infinito. Parece que li mal suas mensagens então. Desculpe pela confusão.

Então, parece que um bom próximo passo seria espalhar llvm.sideffect no IR que geramos e fazer alguns benchmarks?

Em C, presume-se que um thread de execução termine, execute acessos de memória volátil, E / S ou uma operação atômica de sincronização.

A propósito, isso não é totalmente correto - um loop com uma condicional constante (como while (true) { /* ... */ } ) é explicitamente permitido pelo padrão, mesmo que não contenha quaisquer efeitos colaterais. Isso é diferente em C ++. O LLVM não implementa o padrão C corretamente aqui.

Fiquei com a impressão de que você argumentou acima que este não é o mesmo bug que o problema do loop infinito.

O comportamento de programas Rust sem terminação é sempre definido, enquanto o comportamento de programas LLVM-IR sem terminação é definido apenas se certas condições forem atendidas.

Eu pensei que esse problema era sobre como consertar a implementação do Rust para loops infinitos de forma que o comportamento do LLVM-IR gerado fosse definido e, para isso, @llvm.sideeffect parecia uma solução muito boa.

@SergioBenitez mencionou que também se pode criar programas Rust sem terminação usando recursão, e @rkruppe argumentou que recursão infinita e loops infinitos são equivalentes, de modo que ambos são o mesmo bug.

Não discordo que esses dois problemas estejam relacionados, ou mesmo que sejam o mesmo bug, mas para mim, esses dois problemas parecem um pouco diferentes:

  • Em termos de solução, passamos da aplicação de uma barreira de otimização ( @llvm.sideeffect ) exclusivamente para loops não terminados, para aplicá-la a cada função Rust.

  • em termos de valor, loop s infinitos são úteis porque o programa nunca termina. Para recursão infinita, se o programa termina depende do nível de otimização (por exemplo, se o LLVM transforma a recursão em um loop ou não), e quando e como o programa termina depende da plataforma (tamanho da pilha, página de guarda protegida, etc.). A correção de ambos é necessária para fazer a implementação do Rust funcionar, mas no caso de recursão infinita, se o usuário pretendia que seu programa recursasse para sempre, uma implementação sólida ainda estaria "errada" no sentido de que nem sempre recursaria para sempre.

Em termos de solução, passamos da aplicação de uma barreira de otimização (@ llvm.sideeffect) exclusivamente para loops sem terminação, para aplicá-la a cada função Rust.

A análise necessária para mostrar que um corpo de loop realmente tem efeitos colaterais (não apenas potencialmente , como acontece com chamadas para funções externas) e, portanto, não precisa de uma inserção de llvm.sideeffect é bastante complicada, provavelmente na mesma ordem de magnitude como mostrando o mesmo para uma função que pode fazer parte da recursão infinita. Provar que um loop está terminando também é difícil sem fazer muitas otimizações primeiro, uma vez que a maioria dos loops Rust envolve iteradores. Então, acho que acabaríamos colocando llvm.sideeffect na grande maioria dos loops, independentemente. É verdade que existem algumas funções que não contêm loops, mas ainda não parece uma diferença qualitativa para mim.

Se entendi o problema corretamente, para corrigir o caso do loop infinito, deve ser suficiente inserir llvm.sideeffect em loop { ... } e while <compile-time constant true> { ... } onde o corpo do loop não contém break expressões. Isso captura a diferença entre a semântica do C ++ e a semântica do Rust para loops infinitos: no Rust, ao contrário do C ++, o compilador não tem permissão para assumir que um loop termina quando é conhecido no momento da compilação que _não_. (Não tenho certeza do quanto precisamos nos preocupar com a correção diante de loops em que o corpo pode entrar em pânico, mas isso sempre pode ser melhorado mais tarde.)

Não sei o que fazer sobre a recursão infinita, mas concordo com RalfJung que otimizar uma recursão infinita em um segfault não relacionado não é um comportamento desejável.

@zackw

Se entendi o problema corretamente, para corrigir o caso do loop infinito, deve ser suficiente inserir llvm.sideeffect no loop {...} e while{...} onde o corpo do loop não contém expressões de interrupção.

Não acho que seja tão simples, por exemplo, loop { if false { break; } } é um loop infinito que contém uma expressão break , mas precisamos inserir @llvm.sideeffect para evitar que o llvm o remova. AFAICT, temos que inserir @llvm.sideeffect menos que possamos provar que o loop sempre termina.

@gnzlbg

loop { if false { break; } } é um loop infinito que contém uma expressão de quebra, mas precisamos inserir @llvm.sideeffect para evitar que o llvm o remova.

Hm, sim, isso é problemático. Mas não temos que ser _perfeitos_, apenas corretos conservadoramente. Um loop como

while spinlock.load(Ordering::SeqCst) != 0 {}

(da documentação de std::sync::atomic ) seria facilmente visto como não sendo necessário um @llvm.sideeffect , uma vez que a condição de controle não é constante (e uma operação de carregamento atômico deve contar como um efeito colateral para fins de LLVM , ou temos problemas maiores). O tipo de loop finito que pode ser emitido por um gerador de programa,

loop {
    if /* runtime-variable condition */ { break }
    /* more stuff */
}

também não deve ser problemático. Na verdade, existe algum caso em que a regra "sem quebra de expressões no corpo do loop" esteja errada _além do que_

loop {
    if /* provably false at compile time */ { break }
}

?

Achei que esse problema era sobre como consertar a implementação do Rust para loops infinitos de forma que o comportamento do LLVM-IR gerado fosse definido e, para isso, @ llvm.sideeffect parecia uma solução muito boa.

Justo. No entanto, como você disse, o problema (a incompatibilidade entre a semântica do Rust e a semântica do LLVM) é, na verdade, sobre não terminação, não sobre loops. Então eu acho que é o que devemos rastrear aqui.

@zackw

Se entendi o problema corretamente, para corrigir o caso do loop infinito, deve ser suficiente inserir llvm.sideeffect no loop {...} e while{...} onde o corpo do loop não contém expressões de interrupção. Isso captura a diferença entre a semântica do C ++ e a semântica do Rust para loops infinitos: no Rust, ao contrário do C ++, o compilador não pode assumir que um loop termina quando é conhecível no tempo de compilação, mas não o faz. (Não tenho certeza do quanto precisamos nos preocupar com a correção diante de loops em que o corpo pode entrar em pânico, mas isso sempre pode ser melhorado mais tarde.)

O que você descreve vale para C. No Rust, qualquer loop pode divergir. Todo o resto seria simplesmente inadequado.

Então, por exemplo

while test_fermats_last_theorem_on_some_random_number() { }

é um programa adequado em Rust (mas não em C nem C ++) e fará um loop indefinido sem causar um efeito colateral. Então, tem que ser todos os loops, exceto aqueles que podemos provar que terminarão.

@zackw

há algum caso em que a regra "sem quebra de expressões no corpo do loop" fica errada além de

Não é apenas if /*compile-time condition */ . Todo o fluxo de controle é afetado ( while , match , for , ...) e as condições de tempo de execução também são afetadas.

Mas não temos que ser perfeitos, apenas corretos conservadoramente.

Considerar:

fn foo(x: bool) { loop { if x { break; } } }

onde x é uma condição de tempo de execução. Se não emitirmos @llvm.sideeffect aqui, então se o usuário escrever foo(false) algum lugar, foo poderia ser embutido e com propagação constante e eliminação de código morto, o loop otimizado em um loop infinito sem efeitos colaterais, resultando em uma otimização incorreta.

Se isso fizer sentido, uma transformação que o LLVM teria permissão para fazer é substituir foo por foo_opt :

fn foo_opt(x: bool) { if x { foo(true) } else { foo(false) } }

onde ambos os ramos são otimizados de forma independente, e o segundo ramo seria mal otimizado se não usarmos @llvm.sideeffect .

Ou seja, para poder omitir @llvm.sideeffect , precisaríamos provar que o LLVM não pode otimizar erroneamente aquele loop em nenhuma circunstância. A única maneira de provar isso é provar que o loop sempre termina ou provar que, se ele não terminar, ele realiza incondicionalmente uma das coisas que evita otimizações incorretas. Mesmo assim, otimizações como divisão / remoção de loop poderiam transformar um loop em uma série de loops, e seria o suficiente para um deles não ter @llvm.sideeffect para que uma otimização incorreta acontecesse.

Tudo sobre esse bug me parece que seria muito mais fácil de resolver no LLVM do que em rustc . (isenção de responsabilidade: eu realmente não sei a base de código de nenhum desses projetos)

Pelo que entendi, a correção do LLVM seria alterar as otimizações de execução em (provar não encerramento || não posso provar nada) para execução apenas quando o não encerramento pode ser provado (ou o oposto). Não estou dizendo que isso seja fácil (de forma alguma), mas o LLVM já (eu acho) inclui código para tentar provar o (não) término de loops.

Por outro lado, rustc só pode fazer isso adicionando @llvm.sideeffect , o que potencialmente terá mais impacto na otimização do que “apenas” desabilitar as otimizações que fazem uso impróprio de não terminação. E rustc teria que incorporar um novo código para tentar detectar o (não) término de loops.

Então, eu acho que o caminho a seguir seria:

  1. Adicione @llvm.sideeffect em cada loop e chamada de função para corrigir o problema
  2. Corrija o LLVM para não realizar otimizações erradas em loops sem terminação e remova o @llvm.sideeffects

O que você pensa sobre isso? Espero que o impacto no desempenho da etapa 1 não seja tão horrível, embora seja para desaparecer assim que 2 for implementado ...

@Ekleog é disso que se trata o segundo patch https://lists.llvm.org/pipermail/llvm-dev/2017-October/118595.html

parte da proposta do atributo da função é
alterar a semântica padrão do LLVM IR para ter um comportamento definido em
loops infinitos e, em seguida, adicionar um atributo optando por UB potencial. então
se fizermos isso, o papel de @ llvm.sideeffect se tornará um pouco
sutil - seria uma maneira de um frontend para uma linguagem como C optar
em UB potencial para uma função, mas, em seguida, opte por não participar de
loops nessa função.

Para ser justo com o LLVM, os escritores do compilador não abordam este tópico da perspectiva de "Vou escrever uma otimização que prova que os loops não têm terminação, para que eu possa otimizá-los pedantemente!" Em vez disso, a suposição de que os loops serão encerrados ou terão efeitos colaterais surge naturalmente em alguns algoritmos de compilador comuns. Consertar isso não é apenas um ajuste no código existente; vai exigir uma quantidade significativa de nova complexidade.

Considere o seguinte algoritmo para testar se um corpo funcional "não tem efeitos colaterais": se qualquer instrução no corpo tiver efeitos colaterais potenciais, o corpo funcional pode ter efeitos colaterais. Bom e simples. Posteriormente, as chamadas às funções "sem efeitos colaterais" são excluídas. Legal. Exceto, as instruções de ramificação são consideradas sem efeitos colaterais, de modo que uma função contendo apenas ramificações parecerá não ter efeitos colaterais, embora possa conter um loop infinito. Opa.

Isso pode ser corrigido. Se mais alguém estiver interessado em investigar isso, minha ideia básica é dividir o conceito de "tem efeitos colaterais" em conceitos independentes de "tem efeitos colaterais reais" e "pode ​​não ter efeito colateral". Em seguida, analise todo o otimizador e encontre todos os lugares que se preocupam com "tem efeitos colaterais" e descubra de quais conceitos eles realmente precisam. E então ensine os passes de loop a adicionar metadados a branches que não fazem parte de um loop, ou os loops em que eles estão são comprovadamente finitos, para evitar pessimizações.


Um possível compromisso pode ser rustc insert @ llvm.sideeffect quando um usuário literalmente escreve um loop { } vazio (ou similar) ou recursão incondicional (que já tem um lint). Esse acordo permitiria que as pessoas que realmente pretendem um loop giratório infinito e sem efeito o obtenham, evitando qualquer sobrecarga para todos os outros. Claro, esse compromisso não tornaria impossível travar o código seguro, mas provavelmente reduziria as chances de isso acontecer acidentalmente e parece que deve ser fácil de implementar.

Em vez disso, a suposição de que os loops serão encerrados ou terão efeitos colaterais surge naturalmente em alguns algoritmos de compilador comuns.

Porém, é totalmente anormal se você começar a pensar sobre a correção dessas transformações. Para ser franco, ainda acho que foi um grande erro de C permitir essa suposição, mas tudo bem.

se qualquer instrução no corpo tem efeitos colaterais potenciais, então o corpo funcional pode ter efeitos colaterais.

Há uma boa razão para que a "não rescisão" seja normalmente considerada um efeito quando você começa a olhar para as coisas formalmente. (Haskell não é puro, ele tem dois efeitos: não rescisão e exceções.)

Um possível compromisso pode ser ter rustc insert @ llvm.sideeffect quando um usuário literalmente escreve um loop vazio {} (ou similar) ou recursão incondicional (que já tem um lint). Esse acordo permitiria que as pessoas que realmente pretendem um loop giratório infinito e sem efeito o obtenham, evitando qualquer sobrecarga para todos os outros. Claro, esse compromisso não tornaria impossível travar o código seguro, mas provavelmente reduziria as chances de isso acontecer acidentalmente e parece que deve ser fácil de implementar.

Como você mesmo observou, isso ainda está incorreto. Não creio que devamos aceitar uma "solução" que sabemos ser incorreta. Compiladores são parte integrante de nossa infraestrutura, não devemos apenas esperar que nada dê errado. Essa não é a maneira de construir uma base sólida.


O que aconteceu aqui é que a noção de correção foi construída em torno do que os compiladores faziam, em vez de começar com "O que queremos de nossos compiladores" e então fazer disso sua especificação. Um compilador correto não transforma um programa que sempre diverge em um que termina, ponto final. Acho isso bastante evidente, mas com Rust tendo um sistema de tipos razoável, isso é até mesmo testemunhado claramente nos tipos, e é por isso que o problema está surgindo regularmente.

Dadas as restrições com as quais estamos trabalhando (a saber, LLVM), o que devemos fazer é começar adicionando llvm.sideeffect em lugares suficientes para que cada execução divergente tenha a garantia de "executar" um número infinito delas. Então, alcançamos uma linha de base razoável (como em, sólida e correta) e podemos falar sobre melhorias por meio da remoção dessas anotações quando podemos garantir que não são necessárias.

Para tornar meu ponto mais preciso, acho que o seguinte é uma caixa sólida de Rust, com pick_a_number_greater_2 retornando (não deterministicamente) algum tipo de big-int:

fn test_fermats_last_theorem() -> bool {
  let x = pick_a_number_greater_2();
  let y = pick_a_number_greater_2();
  let z = pick_a_number_greater_2();
  let n = pick_a_number_greater_2();
  // x^n + y^n = z^n is impossible for n > 2
  pow(x, n) + pow(y, n) != pow(z, n)
}

pub fn diverge() -> ! {
  while test_fermats_last_theorem() { }
  // This code is unreachable, as proven by Andrew Wiles
  unsafe { mem::transmute(()) }
}

Se compilarmos esse loop divergente, isso é um bug e deve ser corrigido.

Não temos nem números até agora para quanto desempenho custaria para corrigir isso ingenuamente. Até que o façamos, não vejo razão para interromper deliberadamente programas como o acima.

Na prática, fn foo() { foo() } sempre terminará devido ao esgotamento de recursos, mas como a máquina abstrata Rust tem uma estrutura de pilha infinitamente grande (AFAIK), é válido transformar esse código em fn foo() { loop {} } que nunca terminar (ou muito mais tarde, quando o universo congela). Essa transformação deve ser válida? Eu diria que sim, pois, de outra forma, não podemos realizar otimizações de chamada remota, a menos que possamos provar o encerramento, o que seria lamentável.

Faria sentido ter um unsafe intrínseco que afirmasse que um dado loop, recursão, ... sempre termina? N1528 dá um exemplo onde, se os loops não podem ser assumidos para terminar, a fusão do loop não pode ser aplicada ao código do ponteiro que atravessa as listas vinculadas, porque as listas vinculadas podem ser circulares e provar que uma lista vinculada não é circular não é algo que os compiladores modernos podem Faz.

Concordo plenamente que precisamos corrigir esse problema de solidez para sempre. No entanto, a maneira como fazemos isso deve estar ciente da possibilidade de que "adicionar llvm.sideeffect todos os lugares que não podemos provar que é desnecessário" pode regredir a qualidade do código de programas que são compilados corretamente hoje. Embora essas preocupações sejam, em última análise, anuladas pela necessidade de um compilador de som, pode ser prudente proceder de uma forma que atrase um pouco a correção adequada em troca de evitar regressões de desempenho e melhorar a qualidade de vida do programador Rust médio na média Tempo. Eu proponho:

  • Tal como acontece com outras correções potencialmente regressivas de desempenho para bugs de solidez de longa data (# 10184), devemos implementar a correção por trás de um sinalizador -Z para poder avaliar o impacto de desempenho em bases de código em liberdade.
  • Se o impacto for negligenciável, ótimo, podemos apenas ativar a correção por padrão.
  • Mas se houver regressões reais a partir dele, podemos levar esses dados para o pessoal do LLVM e tentar melhorar o LLVM primeiro (ou podemos escolher comer a regressão e consertá-la mais tarde, mas em qualquer caso, tomaríamos uma decisão informada)
  • Se decidirmos não ativar a correção por padrão devido a regressões, podemos pelo menos seguir em frente com a adição de llvm.sideeffect a loops sintaticamente vazios: eles são bastante comuns e sendo mal compilados levou a várias pessoas gastando muito horas depurando problemas estranhos (# 38136, # 47537, # 54214, e certamente há mais), então, embora essa atenuação não tenha relação com o bug de solidez, teria um benefício tangível para os desenvolvedores enquanto resolvemos as torções da maneira adequada correção de bug.

É certo que essa perspectiva é informada pelo fato de que essa questão já existe há anos. Se fosse uma nova regressão, eu estaria mais aberto para consertá-la mais rapidamente ou reverter o PR que a introduziu.

Enquanto isso, isso deve ser mencionado em https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html , desde que o problema esteja aberto?

Faria sentido ter um unsafe intrínseco que declarasse que um dado loop, recursão, ... sempre termina?

std::hint::reachable_unchecked ?

Por acaso, encontrei este código escrevendo para um sistema de mensagens TCP. Eu tinha um loop infinito como um paliativo até que coloquei um mecanismo real para parar, mas o thread saiu imediatamente.

Caso alguém queira jogar golfe de código de caso de teste:

fn main() {
    (|| loop {})()
}

`` `
$ cargo run --release
Instrução ilegal (core despejado)

Caso alguém queira jogar golfe de código de caso de teste:

pub fn main() {
   (|| loop {})()
}

Com o sinalizador -Z insert-sideeffect rustc, adicionado por @sfanxiang em https://github.com/rust-lang/rust/pull/59546, ele continua em loop :)

antes:

main:
  ud2

depois de:

main:
.LBB0_1:
  jmp .LBB0_1

A propósito, o rastreamento de bug do LLVM é https://bugs.llvm.org/show_bug.cgi?id=965 , que ainda não vi postado neste tópico.

@RalfJung Você pode atualizar o hiperlink https://github.com/simnalamburt/snippets/blob/master/rust/src/bin/infinite.rs na descrição do problema em https://github.com/simnalamburt/snippets/blob /12e73f45f3/rust/infinite.rs isso? O hiperlink anterior foi quebrado por um longo tempo, pois não era um link permanente. Obrigado! 😛

@simnalamburt pronto, obrigado!

Aumentar o nível de opção de MIR parece evitar a otimização equivocada no seguinte caso:

pub fn main() {
   (|| loop {})()
}

--emit=llvm-ir -C opt-level=1

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  unreachable
}

--emit=llvm-ir -C opt-level=1 -Z mir-opt-level=2

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  br label %bb1, !dbg !10

bb1:                                              ; preds = %bb1, %start
  br label %bb1, !dbg !11
}

https://godbolt.org/z/N7VHnj

rustc 1.45.0-nightly (5fd2f06e9 2020-05-31)

pub fn oops() {
   (|| loop {})() 
}

pub fn main() {
   oops()
}

Ajudou nesse caso especial, mas não resolve o problema em geral. https://godbolt.org/z/5hv87d

Em geral, esse problema só pode ser resolvido quando rustc ou LLVM podem provar que uma função pura é total antes de usar quaisquer otimizações relevantes.

Na verdade, eu não estava afirmando que isso resolvia o problema. O efeito sutil foi interessante o suficiente para os outros e também valeu a pena mencioná-lo aqui. -Z insert-sideeffect continua corrigindo ambos os casos.

Algo está mudando no lado do LLVM: há uma proposta para adicionar um atributo de nível de função para controlar as garantias de progresso. https://reviews.llvm.org/D85393

Não sei por que todos (aqui e nos encadeamentos do LLVM) parecem estar enfatizando a cláusula sobre o progresso de avanço.

A eliminação do loop parece ser uma consequência direta de um modelo de memória: cálculos de valores podem ser movidos, desde que ocorram - antes do uso do valor. Agora, se houver uma prova de que o valor não pode ser usado, é a prova de que não há acontecimento antes, e o código pode ser movido infinitamente para o futuro e ainda satisfazer o modelo de memória.

Ou, se você não estiver familiarizado com modelos de memória, considere que todo o loop é abstraído em uma função que calcula um valor. Agora substitua todas as leituras do valor fora do loop por uma chamada dessa função. Essa transformação certamente é válida. Agora, se não houver usos para o valor, não haverá invocações da função que faz o loop infinito.

cálculos de valores podem ser movidos, desde que ocorram - antes do uso do valor. Agora, se houver uma prova de que o valor não pode ser usado, é a prova de que não há acontecimento antes, e o código pode ser movido infinitamente para o futuro e ainda satisfazer o modelo de memória.

Esta declaração está correta apenas se o cálculo for garantido para terminar. A não terminação é um efeito colateral, e assim como você não pode remover um cálculo que imprime no stdout (é "não puro"), você não pode remover um cálculo que não termina.

Não há problema em remover a seguinte chamada de função, mesmo se o resultado não for usado:

fn sideeffect() -> u32 {
  println!("Hello!");
  42
}

fn main() {
  let _ = sideffect(); // May not be removed.
}

Isso é verdadeiro para qualquer tipo de efeito colateral e permanece verdadeiro quando você substitui a impressão por um loop {} .

A reclamação sobre a não rescisão como um efeito colateral requer não apenas um acordo de que é (que não é controverso), mas também um acordo sobre _ quando_ deve ser observado.

A certeza de não terminação é observada, se o loop calcular o valor. A não terminação não é observada, se você tiver permissão para reordenar os cálculos que não dependem do resultado do loop.

Como o exemplo no thread LLVM.

x = y % 42;
if y < 0 return 0;
...

As propriedades de terminação da divisão não têm nada a ver com reordenamento. As CPUs modernas tentarão executar a divisão, a comparação, a previsão do branch e a pré-busca do branch bem-sucedido em paralelo. Portanto, você não tem a garantia de observar a divisão concluída no momento em que observa 0 retornado, se y for negativo. (Por "observar" aqui quero dizer realmente medir com um oscilômetro onde a CPU está, não pelo programa)

Se você não pode observar a divisão concluída, você não pode observar a divisão iniciada. Portanto, a divisão no exemplo acima normalmente permitiria ser reordenada, que é o que um compilador pode fazer:

if y < 0 return 0;
x = y % 42;
...

Digo "normalmente", porque talvez haja idiomas em que isso não seja permitido. Não sei se Rust é uma linguagem assim.

Os loops puros não são diferentes.


Não estou dizendo que não seja um problema. Estou apenas dizendo que a garantia de progresso futuro não é o que permite que isso aconteça.

A alegação de não rescisão como efeito colateral requer não apenas um acordo de que é (que não é controverso), mas também um acordo sobre quando deve ser observado.

O que estou expressando é o consenso de todo o campo de pesquisa de linguagens de programação e compiladores. Claro que você é livre para discordar, mas então você também pode redefinir termos como "correção do compilador" - não é útil para uma discussão com outras pessoas.

Quais são as observações permitidas é sempre definido no nível da fonte. A especificação da linguagem define uma "Máquina Abstrata", que descreve (idealmente em detalhes matemáticos meticulosos) quais são os comportamentos permissíveis observáveis ​​de um programa. Este documento não fala sobre otimizações.

A correção de um compilador é então medida em se os programas que ele produz exibem apenas comportamentos observáveis ​​que a especificação diz que o programa de origem pode ter. É assim que funciona cada linguagem de programação que leva a correção a sério, e é a única maneira que conhecemos de como capturar de maneira precisa quando um compilador está correto.

O que cabe a cada linguagem é definir o que exatamente é considerado observável no nível de origem e quais comportamentos de origem são considerados "indefinidos" e, portanto, podem ser assumidos pelo compilador como nunca ocorrendo. Esse problema surge porque C ++ diz que um loop infinito sem outros efeitos colaterais ("divergência silenciosa") é um comportamento indefinido, mas Rust não diz isso. Isso significa que a não terminação no Rust é sempre observável e deve ser preservada pelo compilador. A maioria das linguagens de programação faz essa escolha, porque a escolha do C ++ pode tornar muito fácil introduzir acidentalmente um comportamento indefinido (e, portanto, bugs críticos) em um programa. Rust faz uma promessa de que nenhum comportamento indefinido pode surgir de um código seguro, e como o código seguro pode conter loops infinitos, segue-se que loops infinitos em Rust devem ser comportamento definido (e, portanto, preservado).

Se essas coisas são confusas, sugiro fazer algumas leituras básicas. Posso recomendar "Tipos e linguagens de programação", de Benjamin Pierce. Você provavelmente também encontrará muitas postagens de blog por aí, embora possa ser difícil julgar o quão bem informado o autor realmente é.

Para concretizar, se o seu exemplo de divisão foi alterado para

x = 42 % y;
if y <= 0 { return 0; }

então espero que você concorde que a condição _não_ pode ser içada acima da divisão, porque isso mudaria o comportamento observável quando y é zero (de falhar para retornar zero).

Da mesma forma, em

x = if y == 0 { loop {} } else { y % 42 };
if y < 0 { return 0; }

a máquina abstrata Rust permite que isso seja reescrito como

if y == 0 { loop {} }
else if y < 0 { return 0; }
x = y % 42;

mas a primeira condição e o loop não podem ser descartados.

Ralf, não pretendo saber metade do que você faz e não quero introduzir novos significados. Concordo totalmente com a definição do que é a correção (a ordem de execução deve corresponder à ordem do programa). Eu só pensei que o "quando" o não-encerramento é observável fazia parte dele, como em: se você não está observando o resultado do loop, você não tem uma testemunha de seu encerramento (portanto, não posso alegar que é incorreto) . Preciso revisitar o modelo de execução.

Obrigado por tolerar comigo

@zackw Obrigado. Esse é um código diferente, o que obviamente resultará em uma otimização diferente.

Minha premissa sobre os loops serem otimizados da mesma forma que a divisão falhou (não consigo ver o resultado da divisão == não consigo ver o loop terminar), então o resto não importa.

@olotenko Não sei o que você quer dizer com "observar o resultado do loop". Um loop sem fim faz todo o programa divergir, o que é considerado um comportamento observável - isso significa que é observável fora do programa. Assim, o usuário pode executar o programa e ver se ele continua indefinidamente. Um programa que dura para sempre não pode ser compilado em um programa que termina, porque isso muda o que o usuário pode observar sobre o programa.

Não importa o que aquele loop estava computando ou se o "valor de retorno" do loop é usado ou não. O que importa é o que o usuário pode observar ao executar o programa. O compilador deve certificar-se de que esse comportamento observável permaneça o mesmo. A não rescisão é considerada observável.

Para dar outro exemplo:

fn main() {
  loop {}
  println!("Hello");
}

Este programa nunca imprimirá nada, por causa do loop. Mas se você otimizou o loop (ou reordenou o loop com a impressão), de repente o programa imprimiria "Hello". Portanto, essas otimizações mudam o comportamento observável do programa e não são permitidas.

@RalfJung está tudo bem, entendi agora. Meu problema original era o papel que a "garantia de progresso para a frente" desempenha aqui. A otimização é totalmente possível a partir da dependência de dados. Meu erro foi que, na verdade, a dependência de dados não faz parte da ordem do programa: é literalmente as expressões totalmente ordenadas de acordo com a semântica da linguagem. Se a ordem do programa for total, então sem garantia de progresso (que podemos reafirmar como "qualquer subcaminho da ordem do programa é finito"), podemos reordenar (na ordem de execução) apenas as expressões que podemos _provar_ como encerrando (e preservando algumas outras propriedades, como observabilidade de ações de sincronização, chamadas de SO, IO, etc).

Preciso pensar um pouco mais sobre isso, mas acho que posso ver a razão pela qual podemos "fingir" que a divisão aconteceu no exemplo com x = y % 42 , mesmo que não seja realmente executado para algumas entradas, mas por que o mesmo não se aplica a loops arbitrários. Quero dizer, as sutilezas da correspondência da ordem total (programa) e da ordem parcial (execução).

Eu acho que "comportamento observável" pode ser um pouco mais sutil do que isso, já que uma recursão infinita vai acabar com uma falha de estouro de pilha ("termina" no sentido de um "usuário observando o resultado"), mas uma otimização de chamada final irá transformá-lo em um loop sem fim. Pelo menos essa é outra coisa que o Rust / LLVM fará. Mas não precisamos discutir essa questão, pois esse não é realmente o motivo do meu problema (a menos que você queira! Com certeza fico feliz em entender se isso é esperado).

estouro de pilha

Estouros de pilha são realmente desafiadores para modelar, boa pergunta. O mesmo para situações de falta de memória. Como uma primeira aproximação, fingimos formalmente que não acontecem. Uma abordagem melhor é dizer que sempre que você chamar uma função, poderá obter um erro devido ao estouro da pilha ou o programa pode continuar - esta é uma escolha não determinística feita em cada chamada. Desta forma, você pode aproximar o que realmente acontece.

podemos reordenar (na ordem de execução) apenas as expressões que podemos provar como terminantes

De fato. Além disso, eles precisam ser "puros", ou seja, livres de efeitos colaterais - você não pode reordenar dois println! . É por isso que geralmente consideramos a não terminação um efeito também, porque então tudo se reduz a "expressões puras podem ser reordenadas" e "expressões não terminadas são impuras" (impuro = tem um efeito colateral).

Divison também é potencialmente impuro, mas apenas quando dividido por 0 - o que causa pânico, ou seja, um efeito de controle. Isso não é observável diretamente, mas indiretamente (por exemplo, fazendo com que o manipulador de pânico imprima algo em stdout, que é então observável). Assim, a divisão só pode ser reordenada se tivermos certeza de que não estamos dividindo por 0.

Tenho alguns códigos de demonstração que acho que podem ser esse problema, mas não tenho certeza. Se necessário, posso colocar isso em um novo relatório de bug.
Coloquei o código em um repositório git em https://github.com/uglyoldbob/rust_demo

Meu loop infinito (com efeitos colaterais) é otimizado e uma instrução trap é gerada.

Não tenho ideia se essa é uma instância desse problema ou de outra coisa ... dispositivos incorporados não são minha especialidade e, com todas essas dependências externas, não tenho ideia do que mais aquele código está fazendo. ^^ Mas seu programa está não é seguro e ele tem um acesso volátil no circuito, então eu diria que é um problema separado. Quando coloco seu exemplo no playground , acho que está compilado corretamente, então suspeito que o problema seja com uma das dependências extras.

Parece que tudo no loop é uma referência a uma variável local (nenhuma escapou para qualquer outro segmento). Nessas circunstâncias, é fácil provar a ausência de reservas voláteis e a ausência de efeitos observáveis ​​(nenhuma reserva com a qual possam se sincronizar). Se Rust não adicionar um significado especial aos voláteis, esse loop pode ser reduzido a um loop infinito puro.

@uglyoldbob O que realmente está acontecendo em seu exemplo seria mais claro se llvm-objdump não fosse espetacularmente inútil (e impreciso). Esse bl #4 (que não é realmente uma sintaxe de montagem válida) aqui significa ramificar para 4 bytes após o final da instrução bl , também conhecido como o fim da função main , também conhecida como início da próxima função. A próxima função é chamada (quando eu a construo) _ZN11broken_loop18__cortex_m_rt_main17hbe300c9f0053d54dE , e essa é sua função main real. A função com o nome não mutilado main não é sua função, mas uma função completamente diferente gerada pela macro #[entry] fornecida por cortex-m-rt . Seu código não está sendo otimizado. (Na verdade, o otimizador nem mesmo está em execução, pois você está construindo no modo de depuração.)

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

GuillaumeGomez picture GuillaumeGomez  ·  300Comentários

alexcrichton picture alexcrichton  ·  240Comentários

withoutboats picture withoutboats  ·  202Comentários

aturon picture aturon  ·  417Comentários

nikomatsakis picture nikomatsakis  ·  412Comentários