Design: Por favor, suporte rótulos arbitrários e Gotos.

Criado em 8 set. 2016  ·  159Comentários  ·  Fonte: WebAssembly/design

Gostaria de salientar que não estive envolvido no esforço de montagem da web,
e não estou mantendo compiladores grandes ou amplamente usados ​​(apenas meus próprios
linguagem de brinquedo, pequenas contribuições para o backend do compilador QBE e um
estágio na equipe de compiladores da IBM), mas acabei ficando um pouco ranzinza, e
foi incentivado a compartilhar mais amplamente.

Então, embora eu esteja um pouco desconfortável entrando e sugerindo grandes mudanças
para um projeto no qual não tenho trabalhado... aqui vai:

Minhas reclamações:

Quando estou escrevendo um compilador, a primeira coisa que faço com o alto nível
estrutura -- loops, instruções if e assim por diante -- é validá-los para semântica,
fazer verificação de tipo e assim por diante. A segunda coisa que faço com eles é apenas jogá-los
para fora, e achate para blocos básicos e, possivelmente, para a forma SSA. Em algumas outras partes
do mundo do compilador, um formato popular é o estilo de passagem de continuação. eu não sou
um especialista em compilar com estilo de passagem de continuação, mas também não parece
ser um bom ajuste para os loops e blocos com escopo que a montagem da web parece ter
abraçado.

Eu gostaria de argumentar que um formato mais plano, baseado em goto, seria muito mais útil como
um alvo para desenvolvedores de compiladores e não prejudicaria significativamente a
escrita de um polyfill utilizável.

Pessoalmente, também não sou um grande fã de expressões complexas aninhadas. Eles são um pouco
mais desajeitado de consumir, especialmente se os nós internos podem ter efeitos colaterais, mas eu
não se oponha fortemente a eles como um implementador de compilador - O assembly da web
JIT pode consumi-los, posso ignorá-los e gerar as instruções que mapeiam
para o meu RI. Eles não me fazem querer virar a mesa.

O maior problema se resume a loops, blocos e outros elementos sintáticos
que, como um escritor de compilador otimizado, você se esforça muito para representar como um
gráfico com ramos representando arestas; As construções de fluxo de controle explícito
são um obstáculo. Reconstruindo-os a partir do gráfico, uma vez que você realmente fez
as otimizações desejadas são certamente possíveis, mas é um pouco
complexidade para contornar um formato mais complexo. E isso me incomoda: tanto o
produtor e consumidor estão trabalhando em torno de problemas inteiramente inventados
o que seria evitado simplesmente descartando construções complexas de fluxo de controle
da montagem da web.

Além disso, a insistência de construtos de nível superior leva a alguns
casos patológicos. Por exemplo, o dispositivo de Duff acaba com uma web horrível
saída do assembly, como visto brincando no The Wasm Explorer .
No entanto, o inverso não é verdadeiro: tudo o que pode ser expresso
no web assembler pode ser trivialmente convertido para um equivalente em alguns
formato não estruturado, baseado em goto.

Então, no mínimo, gostaria de sugerir que a equipe de montagem da Web adicione
suporte para rótulos arbitrários e gotos. Se eles optarem por manter o maior
construções de nível, seria um pouco de complexidade e desperdício, mas pelo menos
escritores de compiladores como eu seriam capazes de ignorá-los e gerar saída
diretamente.

Polipreenchimento:

Uma das preocupações que ouvi ao discutir isso é que o loop
e a estrutura baseada em blocos permite um polipreenchimento mais fácil da montagem da teia.
Embora isso não seja totalmente falso, acho que uma solução simples de polyfill
para rótulos e gotos é possível. Embora possa não ser tão ideal,
Acho que vale um pouco de feiura no bytecode para
para evitar iniciar uma nova ferramenta com dívida técnica incorporada.

Se assumirmos uma sintaxe do tipo LLVM (ou QBE) para montagem da web, algum código
que se parece com:

int f(int x) {
    if (x == 42)
        return 123;
    else
        return 666;
}

pode compilar para:

 func @f(%x : i32) {
    %1 = test %x 42
jmp %1 iftrue iffalse

 L0:
    %r =i 123
jmp LRet
 L1:
    %r =i 666
jmp LRet
 Lret:
    ret %r
 }

Isso pode ser polyfilled para Javascript que se parece com:

function f(x) {
    var __label = L0;
    var __ret;

    while (__label != LRet) {
        switch (__label) {
        case L0:
            var _v1 = (x == 42)
            if (_v1) {__lablel = L1;} else {label = L2;}
            break;
        case L1:
            __ret = 123
            __label = LRet
            break;
        case L2;
            __ret = 666
            __label = LRet
            break;
        default:
            assert(false);
            break;
    }
}

É feio? Sim. Isso importa? Espero que, se a montagem da web decolar,
não por muito tempo.

E se não:

Bem, se eu conseguisse segmentar a montagem da Web, acho que geraria código
usando a abordagem que mencionei no polyfill, e faço o meu melhor para ignorar todos
as construções de alto nível, esperando que os compiladores fossem inteligentes o suficiente para
agarrar-se a este padrão.

Mas seria bom se não precisássemos ter os dois lados da geração de código
contornar o formato especificado.

control flow

Comentários muito úteis

A próxima versão do Go 1.11 terá suporte experimental para WebAssembly. Isso incluirá suporte completo para todos os recursos do Go, incluindo goroutines, canais, etc. No entanto, o desempenho do WebAssembly gerado atualmente não é tão bom.

Isso se deve principalmente à falta da instrução goto. Sem a instrução goto, tivemos que recorrer ao uso de um loop de nível superior e uma tabela de salto em todas as funções. Usar o algoritmo relooper não é uma opção para nós, pois ao alternar entre goroutines precisamos poder retomar a execução em diferentes pontos de uma função. O relooper não pode ajudar com isso, apenas uma instrução goto pode.

É incrível que o WebAssembly tenha chegado ao ponto em que pode suportar uma linguagem como Go. Mas para ser verdadeiramente o assembly da web, o WebAssembly deve ser tão poderoso quanto outras linguagens assembly. Go possui um compilador avançado capaz de emitir assembly muito eficiente para várias outras plataformas. É por isso que eu gostaria de argumentar que é principalmente uma limitação do WebAssembly e não do compilador Go que não é possível usar também este compilador para emitir assembly eficiente para a web.

Todos 159 comentários

@oridb Wasm é um pouco otimizado para que o consumidor possa converter rapidamente para o formulário SSA, e a estrutura ajuda aqui para padrões de código comuns, portanto, a estrutura não é necessariamente um fardo para o consumidor. Discordo da sua afirmação de que 'ambos os lados da geração de código funcionam em torno do formato especificado'. Wasm é muito sobre um consumidor magro e rápido, e se você tiver algumas propostas para torná-lo mais magro e rápido, isso pode ser construtivo.

Os blocos que podem ser ordenados em um DAG podem ser expressos nos blocos e ramificações wasm, como seu exemplo. O switch-loop é o estilo usado quando necessário, e talvez os consumidores possam fazer algum jump threading para ajudar aqui. Talvez dê uma olhada no binaryen que pode fazer muito do trabalho para o backend do seu compilador.

Houve outros pedidos de suporte CFG mais geral, e algumas outras abordagens usando loops mencionados, mas talvez o foco esteja em outro lugar no momento.

Eu não acho que haja planos para oferecer suporte a 'estilo de passagem de continuação' explicitamente na codificação, mas houve menção de blocos e loops exibindo argumentos (assim como um lambda) e suportando vários valores (vários argumentos lambda) e adicionando um pick para facilitar a referência de definições (os argumentos lambda).

a estrutura ajuda aqui para padrões de código comuns

Não estou vendo nenhum padrão de código comum que seja mais fácil de representar em termos de ramificações para rótulos arbitrários, versus os loops restritos e o subconjunto de blocos que o assembly da Web impõe. Eu poderia ver um pequeno benefício se houvesse uma tentativa de fazer o código se assemelhar ao código de entrada para certas classes de linguagem, mas isso não parece ser um objetivo - e as construções são um pouco vazias se estivessem lá para

Os blocos que podem ser ordenados em um DAG podem ser expressos nos blocos e ramificações wasm, como seu exemplo.

Sim, podem ser. No entanto, eu prefiro não adicionar trabalho extra para determinar quais podem ser representados dessa maneira, versus quais precisam de trabalho extra. Realisticamente, eu pularia a análise extra e sempre geraria o formulário de loop de comutação.

Novamente, meu argumento não é que loops e blocos tornam as coisas impossíveis; É que tudo o que eles podem fazer é mais simples e fácil para uma máquina escrever com goto, goto_if e rótulos arbitrários e não estruturados.

Talvez dê uma olhada no binaryen que pode fazer muito do trabalho para o backend do seu compilador.

Eu já tenho um back-end útil com o qual estou bastante satisfeito e pretendo inicializar totalmente o compilador inteiro em minha própria linguagem. Prefiro não adicionar uma dependência extra bastante grande simplesmente para contornar o uso forçado de loops/blocos. Se eu simplesmente usar loops de comutação, emitir o código é bastante trivial. Se eu tentar realmente usar os recursos presentes na montagem da Web de forma eficaz, em vez de fazer o meu melhor para fingir que eles não existem, torna-se muito mais desagradável.

Houve outros pedidos de suporte CFG mais geral, e algumas outras abordagens usando loops mencionados, mas talvez a força esteja em outro lugar no momento.

Ainda não estou convencido de que os loops tenham algum benefício - qualquer coisa que possa ser representada com um loop pode ser representada com um goto e um rótulo, e há conversões rápidas e conhecidas para SSA a partir de listas de instruções simples.

No que diz respeito ao CPS, não acho que seja necessário suporte explícito - é popular nos círculos de FP porque é bastante fácil de converter diretamente para assembly e oferece benefícios semelhantes ao SSA em termos de raciocínio (http:// mlton.org/pipermail/mlton/2003-January/023054.html); Novamente, não sou especialista nisso, mas pelo que me lembro, a continuação da invocação é reduzida a um rótulo, alguns movimentos e um goto.

@oridb 'há conversões rápidas e bem conhecidas para SSA de listas de instruções simples'

Seria interessante saber como eles se comparam com os decodificadores wasm SSA, essa é a questão importante?

Wasm faz uso de uma pilha de valores no momento, e alguns dos benefícios disso seriam sem a estrutura, prejudicaria o desempenho do decodificador. Sem a pilha de valores, a decodificação SSA também teria mais trabalho, tentei um código base de registro e a decodificação foi mais lenta (não tenho certeza de quão significativo isso é).

Você manteria a pilha de valores ou usaria um design baseado em registro? Se manter a pilha de valores, talvez se torne um clone do CIL, e talvez o desempenho do wasm possa ser comparado ao CIL, alguém realmente verificou isso?

Você manteria a pilha de valores ou usaria um design baseado em registro?

Eu realmente não tenho nenhum sentimento forte nesse sentido. Eu imagino que a compactação da codificação seria uma das maiores preocupações; Um design de registro pode não se sair tão bem lá - ou pode acabar comprimindo fantasticamente sobre gzip. Eu realmente não sei de cabeça.

O desempenho é outra preocupação, embora eu suspeite que possa ser menos importante devido à capacidade de armazenar em cache a saída binária, além do fato de que o tempo de download pode superar a decodificação em ordens de magnitude.

Seria interessante saber como eles se comparam com os decodificadores wasm SSA, essa é a questão importante?

Se você estiver decodificando para SSA, isso significa que você também estaria fazendo uma quantidade razoável de otimização. Eu estaria curioso para avaliar o quão significativo é o desempenho de decodificação em primeiro lugar. Mas, sim, essa é definitivamente uma boa pergunta.

Obrigado por suas perguntas e preocupações.

Vale a pena notar que muitos dos designers e implementadores de
WebAssembly tem experiência em JITs industriais de alto desempenho, não apenas
para JavaScript (V8, SpiderMonkey, Chakra e JavaScriptCore), mas também em
LLVM e outros compiladores. Eu pessoalmente implementei dois JITs para Java
bytecode e posso atestar que uma máquina de pilha com gotos irrestritos
introduz bastante complexidade na decodificação, verificação e construção de um
compilador IR. Na verdade, existem muitos padrões que podem ser expressos em Java
bytecode que causará JITs de alto desempenho, incluindo C1 e C2 em
HotSpot para simplesmente desistir e relegar o código para ser executado apenas no
intérprete. Em contraste, construir um compilador IR a partir de algo como um
AST de JavaScript ou outra linguagem é algo que também fiz. O
estrutura extra de um AST torna parte desse trabalho muito mais simples.

O design das construções de fluxo de controle do WebAssembly simplifica os consumidores ao
permitindo verificação rápida e simples, fácil, conversão de uma passagem para o formulário SSA
(mesmo um gráfico IR), JITs de passagem única eficazes e (com pós-ordem e o
máquina de pilha) interpretação no local relativamente simples. Estruturada
controle impossibilita gráficos de fluxo de controle irredutíveis, o que elimina
uma classe inteira de casos de canto desagradáveis ​​para decodificadores e compiladores. Isso também
prepara muito bem o cenário para o tratamento de exceções no bytecode WASM, para o qual o V8
já está desenvolvendo um protótipo em conjunto com a produção
implementação.

Tivemos muitas discussões internas entre os membros sobre isso
tópico, já que, para um bytecode, é uma coisa que é mais diferente de
outros destinos no nível da máquina. No entanto, não é diferente de segmentar
uma linguagem fonte como JavaScript (que muitos compiladores fazem hoje em dia) e
requer apenas uma pequena reorganização de blocos para alcançar a estrutura. Lá
são algoritmos conhecidos para fazer isso, e ferramentas. Gostaríamos de fornecer alguns
melhor orientação para os produtores que começam com um CFG arbitrário para
comunicar isso melhor. Para idiomas que segmentam WASM diretamente de um AST
(que na verdade é algo que o V8 faz agora para o código asm.js - diretamente
traduzindo um bytecode JavaScript AST para WASM), não há reestruturação
passo necessário. Esperamos que este seja o caso de muitas ferramentas de linguagem
em todo o espectro que não possuem IRs sofisticados.

Na quinta-feira, 8 de setembro de 2016 às 9h53, Ori Bernstein [email protected]
escreveu:

Você manteria a pilha de valores ou usaria um design baseado em registro?

Eu realmente não tenho nenhum sentimento forte nesse sentido. eu imagino
a compactação da codificação seria uma das maiores preocupações; Como você
mencionado, o desempenho é outro.

Seria interessante saber como eles se comparam com os decodificadores wasm SSA, que
é a pergunta importante?

Se você estiver decodificando para SSA, isso significa que você também estaria fazendo um
quantidade razoável de otimização. Eu estaria curioso para avaliar como
desempenho de decodificação significativo está em primeiro lugar. Mas, sim, isso é
definitivamente uma boa pergunta.


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment -245521009,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/ALnq1Iz1nn4--NL32R9ev0JPKfEnDyvqks5qn77cgaJpZM4J3ofA
.

Obrigado @titzer , eu estava desenvolvendo uma suspeita de que a estrutura do Wasm tinha um propósito além da semelhança com o asm.js. Eu me pergunto: o bytecode Java (e CIL) não modela CFGs ou a pilha de valor diretamente, eles precisam ser inferidos pelo JIT. Mas no Wasm (especialmente se assinaturas de bloco são adicionadas) o JIT pode facilmente descobrir o que está acontecendo com a pilha de valor e o fluxo de controle, então eu me pergunto se CFGs (ou fluxo de controle irredutível especificamente) foram modelados explicitamente como loops e blocos são, isso pode evitar a maioria dos casos de canto desagradáveis ​​que você está pensando?

Há essa otimização pura que os intérpretes usam que se baseia no fluxo de controle irredutível para melhorar a previsão de desvios...

@oridb

Eu gostaria de argumentar que um formato mais plano, baseado em goto, seria muito mais útil como
um alvo para desenvolvedores de compiladores

Concordo que gotos são muito úteis para muitos compiladores. É por isso que ferramentas como o Binaryen permitem que você gere CFGs arbitrários com gotos , e eles podem convertê-los de maneira rápida e eficiente em WebAssembly para você.

Pode ajudar pensar no WebAssembly como algo otimizado para os navegadores consumirem (como @titzer apontou). A maioria dos compiladores provavelmente não deve gerar o WebAssembly diretamente, mas sim usar uma ferramenta como o Binaryen, para que possam emitir gotos, obter várias otimizações gratuitamente e não precisar pensar em detalhes de formato binário de baixo nível do WebAssembly (em vez disso, você emite um IR usando uma API simples).

Em relação ao polyfilling com o padrão while-switch você menciona: em emscripten começamos assim antes de desenvolvermos o método "relooper" de recriar loops. O padrão while-switch é cerca de 4x mais lento em média (mas em alguns casos significativamente menos ou mais, por exemplo, pequenos loops são mais sensíveis). Concordo com você que, em teoria, as otimizações de jump-threading podem acelerar isso, mas o desempenho será menos previsível, pois algumas VMs o farão melhor do que outras. Também é significativamente maior em termos de tamanho de código.

Pode ajudar pensar no WebAssembly como algo otimizado para os navegadores consumirem (como @titzer apontou). A maioria dos compiladores provavelmente não deve gerar o WebAssembly diretamente, mas sim usar uma ferramenta como Binaryen...

Ainda não estou convencido de que esse aspecto seja muito importante - novamente, suspeito que o custo de buscar o bytecode dominaria o atraso que o usuário vê, com o segundo maior custo sendo as otimizações feitas, e não a análise e validação . Também estou assumindo/esperando que o bytecode seja descartado e a saída compilada seja o que seria armazenado em cache, tornando a compilação efetivamente um custo único.

Mas se você estava otimizando para o consumo do navegador da web, por que não simplesmente definir a montagem da web como SSA, o que me parece mais alinhado com o que eu esperaria e menos esforço para 'converter' para SSA?

Você pode começar a analisar e compilar durante o download, e algumas VMs podem não fazer uma compilação completa antecipadamente (podem usar apenas uma linha de base simples, por exemplo). Portanto, os tempos de download e compilação podem ser menores do que o esperado e, como resultado, a análise e a validação podem ser um fator significativo no atraso total que o usuário vê.

Em relação às representações SSA, elas tendem a ter tamanhos de código grandes. O SSA é ótimo para otimizar código, mas não para serializar código de forma compacta.

@oridb Veja o comentário de @titzer 'O design das construções de fluxo de controle do WebAssembly simplifica os consumidores, permitindo uma verificação rápida e simples, uma conversão fácil de

Grande parte da eficiência de codificação do wasm parece vir da otimização para o padrão de código comum no qual as definições têm um único uso que é usado na ordem da pilha. Espero que uma codificação SSA também possa fazer isso, para que possa ter uma eficiência de codificação semelhante. Operadores como if_else para padrões de diamante também ajudam muito. Mas sem a estrutura wasm, parece que todos os blocos básicos precisariam ler definições de registradores e gravar resultados em registradores, e isso pode não ser tão eficiente. Por exemplo, acho que wasm pode fazer ainda melhor com um operador pick que pode referenciar valores de pilha com escopo na pilha e através dos limites básicos do bloco.

Acho que wasm não está muito longe de ser capaz de codificar a maioria dos códigos no estilo SSA. Se as definições fossem passadas para a árvore de escopo como saídas de blocos básicos, então ela poderia estar completa. A codificação SSA pode ser ortogonal ao assunto CFG. Por exemplo, pode haver uma codificação SSA com as restrições wasm CFG, pode haver uma VM baseada em registro com as restrições CFG.

Um objetivo do wasm é remover a carga de otimização do consumidor de tempo de execução. Há uma forte resistência em adicionar complexidade no compilador de tempo de execução, pois aumenta a superfície de ataque. Grande parte do desafio de design é perguntar o que pode ser feito para simplificar o compilador de tempo de execução sem prejudicar o desempenho e muito debate!

Bem, provavelmente é tarde demais agora, mas eu gostaria de questionar a ideia de que o algoritmo do relooper, ou suas variantes, podem produzir resultados "suficientemente bons" em todos os casos. Eles claramente podem na maioria dos casos, já que a maioria dos códigos-fonte não contém fluxo de controle irredutível para começar, as otimizações geralmente não tornam as coisas muito complicadas e, se o fizerem, por exemplo, como parte da mesclagem de blocos duplicados, provavelmente podem ser ensinados não. Mas e os casos patológicos? Por exemplo, e se você tiver uma corrotina que um compilador transformou em uma função regular com estrutura como este pseudo-C:

void transformed_coroutine(struct autogenerated_context_struct *ctx) {
    int arg1, arg2; // function args
    int var1, var2, var3, …; // all vars used by the function
    switch (ctx->current_label) { // restore state
    case 0:
        // initial state, load function args caller supplied and proceed to start
        arg1 = ctx->arg1;
        arg2 = ctx->arg2;
        break;
    case 1: 
        // restore all vars which are live at label 1, then jump there
        var2 = ctx->var2; 
        var3 = ctx->var3;
        goto resume_1;
    [more cases…]
    }

    [main body goes here...]
    [somewhere deep in nested control flow:]
        // originally a yield/await/etc.
        ctx->var2 = var2;
        ctx->var3 = var3;
        ctx->current_label = 1;
        return;
        resume_1:
        // continue on
}

Então você tem fluxo de controle quase normal, mas com alguns gotos apontados no meio dele. É mais ou menos assim que as corrotinas do LLVM funcionam .

Eu não acho que haja uma boa maneira de refazer algo assim, se o fluxo de controle 'normal' for complexo o suficiente. (Pode estar errado.) Ou você duplica partes maciças da função, potencialmente precisando de uma cópia separada para cada ponto de rendimento, ou transforma a coisa toda em um switch gigante, que de acordo com @kripken é 4x mais lento que o relooper no código típico ( que em si é provavelmente um pouco mais lento do que não precisar de relooper).

A VM poderia reduzir a sobrecarga de um switch gigante com otimizações de jump threading, mas certamente é mais caro para a VM realizar essas otimizações, essencialmente adivinhando como o código se reduz a gotos, do que apenas aceitar gotos explícitos. Como diz @kripken , também é menos previsível.

Talvez fazer esse tipo de transformação seja uma má ideia para começar, já que depois nada domina nada, então as otimizações baseadas em SSA não podem fazer muito… Mas o compilador pode realizar a maioria das otimizações antes de fazer a transformação, e parece que pelo menos os designers de corrotinas LLVM não viram uma necessidade urgente de atrasar a transformação até a geração do código. Por outro lado, como há uma grande variedade na semântica exata que as pessoas querem das corrotinas (por exemplo, duplicação de corrotinas suspensas, capacidade de inspecionar 'quadros empilhados' para GC), quando se trata de projetar um bytecode portátil (em vez de um compilador), é mais flexível oferecer suporte adequado ao código já transformado do que fazer com que a VM faça a transformação.

De qualquer forma, as corrotinas são apenas um exemplo. Outro exemplo que posso pensar é implementar uma VM dentro de uma VM. Enquanto um recurso mais comum dos JITs são as saídas laterais, que não exigem goto, há situações que exigem entradas secundárias - novamente, exigindo goto no meio de loops e tal. Outra seria intérpretes otimizados: não que os intérpretes direcionados ao wasm possam realmente corresponder àqueles direcionados ao código nativo, que no mínimo pode melhorar o desempenho com gotos computados e pode mergulhar em assembly para mais … preditor de ramificação dando a cada caso sua própria instrução de salto, para que você possa replicar parte do efeito tendo um switch separado após cada manipulador de opcode, onde todos os casos seriam apenas gotos. Ou pelo menos tenha um if ou dois para verificar instruções específicas que geralmente vêm após a atual. Existem alguns casos especiais desse padrão que podem ser representados com fluxo de controle estruturado, mas não o caso geral. E assim por diante…

Certamente há alguma maneira de permitir o fluxo de controle arbitrário sem fazer a VM fazer muito trabalho. Idéia do homem de palha, pode ser quebrada: você poderia ter um esquema onde saltos para escopos filho são permitidos, mas apenas se o número de escopos que você precisa inserir for menor que um limite definido pelo bloco de destino. O limite padrão seria 0 (sem saltos dos escopos pai), o que preserva a semântica atual, e o limite de um bloco não pode ser maior que o limite do bloco pai + 1 (fácil de verificar). E a VM mudaria sua heurística de dominância de "X domina Y se for um pai de Y" para "X domina Y se for um pai de Y com distância maior que o limite de salto filho de Y". (Esta é uma aproximação conservadora, não garantida para representar o conjunto dominador exato, mas o mesmo é verdade para a heurística existente - é possível que um bloco interno domine a metade inferior de um externo.) Como somente código com fluxo de controle irredutível precisaria especificar um limite, não aumentaria o tamanho do código no caso comum.

Edit: Curiosamente, isso basicamente transformaria a estrutura de blocos em uma representação da árvore de dominância. Acho que seria muito mais simples expressar isso diretamente: uma árvore de blocos básicos, onde um bloco pode pular para um irmão, ancestral ou filho imediato, mas não para um descendente posterior. Não tenho certeza de como isso mapeia melhor a estrutura de escopo existente, onde um "bloco" pode consistir em vários blocos básicos com sub-loops entre eles.

FWIW: Wasm tem um design particular, que é explicado em apenas algumas palavras muito significativas "exceto que a restrição de aninhamento torna impossível ramificar para o meio de um loop de fora do loop".

Se fosse apenas um DAG, a validação poderia apenas verificar se as ramificações foram encaminhadas, mas com loops isso permitiria ramificar para o meio do loop de fora do loop, daí o design de bloco aninhado.

O CFG é apenas parte deste design, o outro é o fluxo de dados, e há uma pilha de valores e blocos também podem ser organizados para desenrolar a pilha de valores que pode comunicar muito útil o intervalo ao vivo para o consumidor, o que economiza trabalho convertendo para SSA .

É possível estender wasm para ser uma codificação SSA (adicionar pick , permitir que os blocos retornem vários valores e ter entradas de loop com valores pop), então, curiosamente, as restrições exigidas para uma decodificação SSA eficiente podem não ser necessárias (porque já poderia ser codificado em SSA)! Isso leva a uma linguagem funcional (que pode ter uma codificação de estilo de pilha para eficiência).

Se isso fosse estendido para lidar com CFG arbitrário, poderia se parecer com o seguinte. Esta é uma codificação de estilo SSA, portanto, os valores são constantes. Parece ainda se encaixar no estilo da pilha em grande parte, mas não tenho certeza de todos os detalhes. Portanto, dentro de blocks ramificações podem ser feitas para qualquer outro bloco rotulado nesse conjunto, ou alguma outra convenção usada para transferir o controle para outro bloco. O código dentro do bloco ainda pode referenciar valores na pilha de valores mais acima na pilha para economizar a passagem de todos eles.

(func f1 (arg1)
  (let ((c1 10)) ; Some values up the stack.
    (blocks ((b1 (a1 a2 a3)
                   ... (br b3)
               (br b2 (+ a1 a2 a3 arg1 c1)))
             (b2 (a1)
                 ... (br b1 ...))
             (b3 ()
                 ...))
   .. regular structured wasm ..
   (br b2 ...)
   ....
   (br b3)
    ...
   ))

Mas os navegadores da Web lidariam com isso de forma eficiente internamente?

Alguém com um plano de fundo de máquina de pilha reconheceria o padrão de código e seria capaz de combiná-lo com uma codificação de pilha?

Há alguma discussão interessante sobre loops irredutíveis aqui http://bboissin.appspot.com/static/upload/bboissin-thesis-2010-09-22.pdf

Eu não segui tudo em um passe rápido, mas menciona a conversão de loops irredutíveis em loops redutíveis adicionando um nó de entrada. Para o wasm, soa como adicionar uma entrada definida aos loops que é especificamente para despachar dentro do loop, semelhante à solução atual, mas com uma variável definida para isso. O acima menciona que isso é virtualizado, otimizado, no processamento. Talvez algo assim poderia ser uma opção?

Se isso está no horizonte, e dado que os produtores já precisam usar uma técnica semelhante, mas usando uma variável local, então vale a pena considerar agora para que o wasm produzido cedo tenha potencial para rodar mais rápido em tempos de execução mais avançados? Isso também pode criar um incentivo para a competição entre os tempos de execução para explorar isso.

Isso não seria exatamente rótulos e gotos arbitrários, mas algo em que eles possam ser transformados e que tenham alguma chance de serem compilados de forma eficiente no futuro.

Para que conste , estou fortemente com @oridb e @comex nesta questão.
Acho que esta é uma questão crítica que deve ser abordada antes que seja tarde demais.

Dada a natureza do WebAssembly, quaisquer erros que você cometer agora provavelmente permanecerão nas próximas décadas (veja Javascript!). É por isso que a questão é tão crítica; evite suportar gotos agora por qualquer motivo (por exemplo, para facilitar a otimização, que é --- francamente --- a influência de uma implementação específica sobre uma coisa genérica e, honestamente, acho que é preguiçoso), e você acabará com problemas a longo prazo.

Já posso ver implementações WebAssembly futuras (ou atuais, mas no futuro) tentando reconhecer em casos especiais os padrões usuais de while/switch para implementar rótulos para tratá-los corretamente. Isso é um hack.

O WebAssembly é uma lousa limpa, então agora é a hora de evitar hacks sujos (ou melhor, os requisitos para eles).

@darkuranium :

O WebAssembly, conforme especificado atualmente, já está sendo enviado em navegadores e cadeias de ferramentas, e os desenvolvedores já criaram código que assume a forma definida nesse design. Portanto, não podemos alterar o design de maneira quebrável.

Podemos, no entanto, adicionar ao design de maneira compatível com versões anteriores. Eu não acho que nenhum dos envolvidos pense que goto é inútil. Suspeito que todos nós usamos regularmente goto , e não apenas em maneiras sintáticas de brinquedo.

Neste momento, alguém com motivação precisa apresentar uma proposta que faça sentido e implementá-la. Não vejo tal proposta sendo rejeitada se fornecer dados sólidos.

Dada a natureza do WebAssembly, quaisquer erros que você cometer agora provavelmente permanecerão nas próximas décadas (veja Javascript!). É por isso que a questão é tão crítica; evite suportar gotos agora por qualquer motivo (por exemplo, para facilitar a otimização, que é --- francamente --- a influência de uma implementação específica sobre uma coisa genérica e, honestamente, acho que é preguiçoso), e você acabará com problemas a longo prazo.

Então, vou chamar seu blefe: acho que ter a motivação que você mostra, e não apresentar uma proposta e implementação como detalhei acima, é francamente preguiçoso.

Estou sendo insolente, é claro. Considere que temos pessoas batendo em nossas portas por threads, GC, SIMD, etc - todos apresentando argumentos apaixonados e sensatos sobre por que seu recurso é mais importante - seria ótimo se você pudesse nos ajudar a resolver um desses problemas. Há pessoas fazendo isso para os outros recursos que mencionei. Nenhum por goto até agora. Por favor, familiarize-se com as diretrizes de contribuição deste grupo e divirta-se.

Caso contrário, acho que goto é um ótimo recurso futuro . Pessoalmente, eu provavelmente abordaria outros primeiro, como a geração de código JIT. Esse é o meu interesse pessoal depois de GC e tópicos.

Oi. Estou no meio de escrever uma tradução de webassembly para IR e de volta para webassembly, e tive uma discussão sobre esse assunto com as pessoas.

Eu tenho apontado que o fluxo de controle irredutível é difícil de representar em webassembly. Pode ser problemático para otimizar compiladores que ocasionalmente escrevem fluxos de controle irredutíveis. Isso pode ser algo como o loop under, que tem vários pontos de entrada:

if (x) goto inside_loop;
// banana
while(y) {
    // things
    inside_loop:
    // do things
}

Os compiladores EBB produziriam o seguinte:

entry:
    cjump x, inside_loop
    // banana
    jump loop

loop:
    cjump y, exit
    // things
    jump inside_loop

inside_loop:
    // do things
    jump loop
exit:
    return

Em seguida, vamos traduzir isso para webassembly. O problema é que, embora tenhamos descompiladores descobertos há muito tempo , eles sempre tiveram a opção de adicionar o goto em fluxos irredutíveis.

Antes de ser traduzido, o compilador vai fazer truques sobre isso. Mas eventualmente você consegue escanear o código e posicionar os começos e finais das estruturas. Você acaba com os seguintes candidatos depois de eliminar os saltos de queda:

<inside_loop, if(x)>
    // banana
<loop °>
<exit if(y)>
    // things
</inside_loop, if(x)>
    // do things
</loop ↑>
</exit>

Em seguida, você precisa construir uma pilha deles. Qual vai para o fundo? É o 'loop interno' ou então é o 'loop'. Não podemos fazer isso, então temos que cortar a pilha e copiar as coisas:

if
    // do things
else
    // banana
end
loop
  br out
    // things
    // do things
end

Agora podemos traduzir isso para webassembly. Perdoe-me, ainda não estou familiarizado com a forma como esses loops são construídos.

Este não é um problema específico se pensarmos em software antigo. É provável que o novo software seja traduzido para montagem da web. Mas o problema está em como nossos compiladores funcionam. Eles fazem o controle de fluxo com blocos básicos há _décadas_ e assumem que está tudo certo.

Tecnicamente, o idioma é traduzido e depois traduzido. Precisamos apenas de um mecanismo que permita que os valores fluam através dos limites sem o drama. O fluxo estruturado é útil apenas para pessoas que pretendem ler o código.

Mas, por exemplo, o seguinte funcionaria tão bem:

    cjump x, label(1)
    // banana
0: label
    cjump y, label(2)
    // things
1: label
    // do things
    jump label(0)
2: label
    // exit as usual, picking the values from the top of the stack.

Os números seriam implícitos, ou seja, quando o compilador vê um 'label', ele sabe que ele inicia um novo bloco estendido e dá a ele um novo número de índice, começando a incrementar a partir de 0.

Para produzir uma pilha estática, você pode rastrear quantos itens estão na pilha quando encontrar um salto para o rótulo. Se houver uma pilha inconsistente após um salto para o rótulo, o programa é inválido.

Se você achar que o acima está ruim, você também pode tentar adicionar um comprimento de pilha explícito em cada rótulo (talvez delta do tamanho da pilha do último rótulo indexado, se o valor absoluto for ruim para compactação) e um marcador para cada salto sobre quantos valores ele copia do topo da pilha durante o salto.

Eu poderia apostar que você não pode enganar o gzip de forma alguma pelo fato de como você representa o fluxo de controle, então você pode escolher o fluxo que é bom para os caras que têm o trabalho mais difícil aqui. (Posso ilustrar com minha cadeia de ferramentas de compilador flexível para a coisa 'superando o gzip', se você quiser, basta me enviar uma mensagem e vamos fazer uma demonstração!)

Eu me sinto como um destroçado agora. Basta reler a especificação do WebAssembly e perceber que o fluxo de controle irredutível é intencionalmente deixado de fora do MVP, talvez pelo motivo de o emscripten ter que resolver o problema nos primeiros dias.

A solução de como lidar com o fluxo de controle irredutível no WebAssembly é explicada no artigo "Emscripten: An LLVM-to-JavaScript Compiler". O relooper reorganiza o programa mais ou menos assim:

_b_ = bool(x)
_b_ == 0 if
  // banana
end
block loop
  _b_ if
    // do things
    _b_ = 0
  else
    y br_if 2
    // things
    _b_ = 1
  end
  br 0
end end

O racional era que o fluxo de controle estruturado ajuda a ler o dump do código-fonte, e acredito que ele ajude as implementações de polyfill.

As pessoas que compilam do webassembly provavelmente se adaptarão para manipular e separar o fluxo de controle recolhido.

Assim:

  • Como mencionado, o WebAssembly agora está estável, então o tempo passou para qualquer reescrita total de como o fluxo de controle é expresso.

    • Em certo sentido, isso é lamentável, porque ninguém realmente testou se uma codificação mais diretamente baseada em SSA poderia ter alcançado a mesma compacidade que o design atual.

    • No entanto, quando se trata de especificar o goto, isso torna o trabalho muito mais fácil! As instruções baseadas em blocos já estão além do bikeshedding, e não é grande coisa esperar que compiladores de produção direcionados ao wasm expressem fluxo de controle redutível usando-os - o algoritmo não é tão difícil. O principal problema é que uma pequena fração do fluxo de controle não pode ser expressa usando-os sem um custo de desempenho. Se resolvermos isso adicionando uma nova instrução goto, não precisaremos nos preocupar tanto com a eficiência da codificação quanto com um redesenho total. O código usando goto ainda deve ser razoavelmente compacto, é claro, mas não precisa competir com outras construções por compactação; é apenas para fluxo de controle irredutível e deve ser usado raramente.

  • A redutibilidade não é particularmente útil.

    • A maioria dos back-ends do compilador usa uma representação SSA baseada em um gráfico de blocos básicos e ramificações entre eles. A estrutura de loop aninhado, a única coisa que a redutibilidade garante, é praticamente descartada no início.

    • Verifiquei as implementações atuais do WebAssembly em JavaScriptCore, V8 e SpiderMonkey, e todas parecem seguir esse padrão. (V8 é mais complicado - algum tipo de representação de "mar de nós" em vez de blocos básicos - mas também joga fora a estrutura de aninhamento.)

    • Exceção : A análise de loop pode ser útil, e todas essas três implementações passam informações para o IR sobre quais blocos básicos são o início dos loops. (Compare com o LLVM que, como um back-end 'pesado' projetado para compilação AOT, o joga fora e o recalcula no back-end. Isso é mais robusto, pois pode encontrar coisas que não parecem loops no código-fonte, mas fazem depois de um monte de otimizações, mas mais lento.)

    • A análise de loop funciona em "loops naturais", que proíbem ramificações no meio do loop que não passam pelo cabeçalho do loop.

    • O WebAssembly deve continuar a garantir que os blocos loop sejam loops naturais.

    • Mas a análise de loop não exige que toda a função seja redutível, nem mesmo o interior do loop: ela apenas proíbe ramificações de fora para dentro. A representação base ainda é um gráfico de fluxo de controle arbitrário.

    • O fluxo de controle irredutível dificulta a compilação do WebAssembly para JavaScript (polyfilling), pois o compilador teria que executar o próprio algoritmo do relooper.

    • Mas o WebAssembly já toma várias decisões que adicionam uma sobrecarga significativa de tempo de execução a qualquer abordagem de compilação para JS (incluindo suporte a acesso de memória desalinhado e interceptação em acessos fora dos limites), sugerindo que não é considerado muito importante.

    • Comparado a isso, tornar o compilador um pouco mais complexo não é grande coisa.

    • Portanto, não acho que haja uma boa razão para não adicionar algum tipo de suporte para fluxo de controle irredutível.

  • A principal informação necessária para construir uma representação SSA (que, por design, deve ser possível em uma passagem) é a árvore dominadora .

    • Atualmente, um backend pode estimar a dominância com base no fluxo de controle estruturado. Se eu entendi a especificação corretamente, as instruções a seguir encerram um bloco básico:

    • block :



      • O BB que inicia o bloco é dominado pelo BB anterior.*


      • O BB que segue o end é dominado pelo BB que inicia o bloco, mas não pelo BB antes de end (porque será pulado se houver uma saída de br ).



    • loop :



      • O BB que inicia o bloco é dominado pelo BB anterior.


      • O BB depois de end é dominado pelo BB antes de end (já que você não pode obter a instrução após end exceto executando end ).



    • if :



      • O lado if, o outro lado e o BB depois de end são todos dominados pelo BB antes de if .



    • br , return , unreachable :



      • (O BB imediatamente após br , return ou unreachable está inacessível.)



    • br_if , br_table :



      • O BB antes de br_if / br_table domina o que vem depois dele.



    • Notavelmente, esta é apenas uma estimativa. Não pode produzir falsos positivos (dizer que A domina B quando na verdade não domina) porque só diz isso quando não há como chegar a B sem passar por A, por construção. Mas pode produzir falsos negativos (dizer que A não domina B quando na verdade domina), e não acho que um algoritmo de passagem única possa detectá-los (pode estar errado).

    • Exemplo falso negativo:

      ```

      bloquear $exterior

      ciclo

      br $exter ;; uma vez que esta quebra incondicionalmente, secretamente domina o BB final

      fim

      fim

    • Mas tudo bem, AFAIK.



      • Falsos positivos seriam ruins, porque, por exemplo, se o bloco básico A dominar o bloco básico B, o código de máquina para B pode usar um registrador definido em A (se nada entre sobrescrever esse registrador). Se A não dominar B, o registrador pode ter um valor lixo.


      • Os falsos negativos são essencialmente ramificações fantasmas que nunca ocorrem. O compilador assume que essas ramificações podem ocorrer, mas não que devam ocorrer, portanto, o código gerado é apenas mais conservador do que o necessário.



    • De qualquer forma, pense em como uma instrução goto deve funcionar em termos de árvore dominadora. Suponha que A domine B, que domina C.

    • Não podemos pular de A para C porque isso pularia B (violando a suposição de dominância). Em outras palavras, não podemos pular para descendentes não imediatos. (E no final do produtor binário, se eles calcularem a verdadeira árvore dominadora, nunca haverá tal salto.)

    • Poderíamos pular com segurança de A para B, mas ir para um descendente imediato não é tão útil. É basicamente equivalente a uma instrução if ou switch, que já podemos fazer (usando a instrução if se houver apenas um teste binário, ou br_table se houver vários).

    • Também seguro, e mais interessante, é pular para um irmão ou irmão de um antepassado. Se pularmos para nosso irmão, preservamos a garantia de que nosso pai domina nosso irmão, porque já devemos ter executado nosso pai para chegar aqui (já que ele também nos domina). Da mesma forma para os ancestrais.

    • Em geral, um binário malicioso pode produzir falsos negativos em dominância dessa maneira, mas como eu disse, esses são (a) já possíveis e (b) aceitáveis.

  • Com base nisso, aqui está uma proposta espantalho:

    • Uma nova instrução do tipo bloco:
    • rótulos tipo de resultado N instr* fim
    • Deve haver exatamente N instruções para filhos imediatos, onde "filho imediato" significa uma instrução do tipo bloco ( loop , block ou labels ) e tudo até o end , ou uma única instrução não-bloco (que não deve afetar a pilha).
    • Em vez de criar um único rótulo como outras instruções do tipo bloco, labels cria N+1 rótulos: N apontando para os N filhos e um apontando para o final do bloco labels . Em cada um dos filhos, os índices de rótulo 0 a N-1 referem-se aos filhos, em ordem, e o índice de rótulo N refere-se ao final.

    Em outras palavras, se você tiver
    loop ;; outer labels 3 block ;; child 0 br X end nop ;; child 1 nop ;; child 2 end end

    Dependendo de X, br se refere a:

    | X | Alvo |
    | ---------- | ------ |
    | 0 | fim do block |
    | 1 | filho 0 (início do block ) |
    | 2 | criança 1 (não) |
    | 3 | criança 2 (não) |
    | 4 | fim de labels |
    | 5 | início do loop externo |

    • A execução começa no primeiro filho.

    • Se a execução chegar ao final de um dos filhos, ela continua para o próximo. Se chegar ao fim do último filho, volta ao primeiro filho. (Isso é para simetria, porque a ordem dos filhos não deve ser significativa.)

    • A ramificação para um dos filhos desenrola a pilha de operandos até sua profundidade no início de labels .

    • O mesmo acontece com a ramificação para o final, mas se o tipo de resultado não for vazio, a ramificação para o final exibe um operando e o empurra após o desenrolamento, semelhante a block .

    • Dominância: O bloco básico antes da instrução labels domina cada uma das crianças, assim como o BB após o final de labels . As crianças não dominam umas às outras ou ao fim.

    • Notas de projeto:

    • N é especificado antecipadamente para que o código possa ser validado em uma passagem. Seria estranho ter que chegar ao final do bloco labels , para saber o número de filhos, antes de saber os alvos dos índices nele.

    • Não tenho certeza se deve haver uma maneira de passar valores na pilha de operandos entre rótulos, mas por analogia com a incapacidade de passar valores para um block ou loop , que pode não ser suportado para iniciar com.

Seria muito bom se fosse possível pular em um loop, não é? IIUC, se esse caso fosse contabilizado, o combo loop desagradável + br_table nunca seria necessário ...

Edit: oh, você pode fazer um loop sem loop pulando para cima em labels . Não posso acreditar que perdi isso.

@qwertie Se um determinado loop não for um loop natural, o compilador de segmentação wasm deve expressá-lo usando labels vez de loop . Nunca deve ser necessário adicionar um switch para expressar o fluxo de controle, se é a isso que você está se referindo. (Afinal, na pior das hipóteses você poderia usar apenas um bloco gigante labels com um rótulo para cada bloco básico na função. Isso não permite que o compilador saiba sobre dominância e loops naturais, então você pode perder otimizações. Mas labels só é necessário nos casos em que essas otimizações não são aplicáveis.)

A estrutura de loop aninhado, a única coisa que a redutibilidade garante, é praticamente descartada no início. [...] Verifiquei as implementações atuais do WebAssembly em JavaScriptCore, V8 e SpiderMonkey, e todas parecem seguir esse padrão.

Não exatamente: pelo menos no SM, o gráfico IR não é um gráfico totalmente geral; assumimos certas invariantes de grafo que resultam de serem geradas a partir de uma fonte estruturada (JS ou wasm) e muitas vezes simplificamos e/ou otimizamos os algoritmos. O suporte a um CFG totalmente geral exigiria a auditoria/alteração de muitas das passagens no pipeline para não assumir essas invariantes (generalizando-as ou pessimizando-as em caso de irredutibilidade) ou duplicação de divisão de nós antecipadamente para tornar o gráfico redutível. Isso é certamente factível, é claro, mas não é verdade que isso seja simplesmente uma questão de ser um gargalo artificial.

Além disso, o fato de que existem muitas opções e diferentes motores farão coisas diferentes sugere que ter o produtor lidando com a irredutibilidade antecipadamente produzirá um desempenho um pouco mais previsível na presença de fluxo de controle irredutível.

Quando discutimos caminhos compatíveis com versões anteriores para estender o wasm com suporte arbitrário a goto no passado, uma grande questão é qual é o caso de uso aqui: é "tornar os produtores mais simples por não ter que executar um algoritmo do tipo relooper" ou é "permitir codegen mais eficiente para fluxo de controle realmente irredutível"? Se for apenas o primeiro, acho que provavelmente gostaríamos de algum esquema de incorporação de rótulos/gotos arbitrários (que é compatível com versões anteriores e também compõe com futuros try/catch estruturados em bloco); é apenas uma questão de ponderar custo/benefício e as questões mencionadas acima.

Mas para o último caso de uso, uma coisa que observamos é que, embora de vez em quando você veja o caso de um dispositivo de Duff à solta (o que não é realmente uma maneira eficiente de desenrolar um loop...), muitas vezes onde você vê a irredutibilidade aparecer onde o desempenho importa são os loops do intérprete. Os loops do intérprete também se beneficiam do encadeamento indireto que precisa de goto calculado. Além disso, mesmo em compiladores offline robustos, os loops do interpretador tendem a obter a pior alocação de registro. Como o desempenho do loop do interpretador pode ser muito importante, uma questão é se o que realmente precisamos é de uma primitiva de fluxo de controle que permita que o mecanismo execute threading indireto e faça regalloc decente. (Esta é uma pergunta aberta para mim.)

@lukewagner
Eu gostaria de ouvir mais detalhes sobre quais passes dependem de invariantes. O projeto que propus, usando uma construção separada para fluxo irredutível, deve tornar relativamente fácil para passagens de otimização como LICM evitar esse fluxo. Mas se houver outros tipos de quebra que não estou pensando, gostaria de entender melhor sua natureza para ter uma ideia melhor de se e como eles podem ser evitados.

Quando discutimos caminhos compatíveis com versões anteriores para estender o wasm com suporte arbitrário a goto no passado, uma grande questão é qual é o caso de uso aqui: é "tornar os produtores mais simples por não ter que executar um algoritmo do tipo relooper" ou é "permitir codegen mais eficiente para fluxo de controle realmente irredutível"?

Para mim é o último; minha proposta espera que os produtores ainda executem um algoritmo do tipo relooper para salvar o backend do trabalho de identificar dominadores e loops naturais, voltando para labels somente quando necessário. No entanto, isso ainda tornaria os produtores mais simples. Se o fluxo de controle irredutível tiver uma grande penalidade, um produtor ideal deve trabalhar muito para evitá-lo, usando heurísticas para determinar se é mais eficiente duplicar código, a quantidade mínima de duplicação que pode funcionar etc. up otimizações de loop, isso não é realmente necessário, ou pelo menos não é mais necessário do que seria com um back-end de código de máquina regular (que tem suas próprias otimizações de loop).

Eu realmente deveria reunir mais dados sobre como o fluxo de controle irredutível é comum na prática…

No entanto, minha crença é que penalizar tal fluxo é essencialmente arbitrário e desnecessário. Na maioria dos casos, o efeito no tempo de execução geral do programa deve ser pequeno. No entanto, se um hotspot incluir fluxo de controle irredutível, haverá uma penalidade severa; no futuro, os guias de otimização do WebAssembly podem incluir isso como uma pegadinha comum e explicar como identificá-lo e evitá-lo. Se minha crença estiver correta, essa é uma forma totalmente desnecessária de sobrecarga cognitiva para programadores. E mesmo quando a sobrecarga é pequena, o WebAssembly já tem sobrecarga suficiente em comparação com o código nativo para evitar qualquer extra.

Estou aberto à persuasão de que minha crença está incorreta.

Como o desempenho do loop do interpretador pode ser muito importante, uma questão é se o que realmente precisamos é de uma primitiva de fluxo de controle que permita que o mecanismo execute threading indireto e faça regalloc decente.

Isso parece interessante, mas acho que seria melhor começar com uma primitiva de propósito geral. Afinal, uma primitiva feita sob medida para intérpretes ainda exigiria backends para lidar com fluxo de controle irredutível; se você vai morder essa bala, também pode apoiar o caso geral.

Alternativamente, minha proposta já pode servir como um primitivo decente para intérpretes. Se você combinar labels com br_table , você terá a capacidade de apontar uma tabela de salto diretamente em pontos arbitrários na função, o que não é muito diferente de um goto calculado. (Ao contrário de um switch C, que pelo menos inicialmente direciona o fluxo de controle para pontos dentro do bloco de switch; se os casos são todos gotos, o compilador deve ser capaz de otimizar o salto extra, mas também pode coalescer vários 'redundantes' switch instruções em uma, arruinando o benefício de ter um salto separado após cada manipulador de instruções.) Não tenho certeza de qual é o problema com a alocação de registradores, no entanto ...

@comex Acho que alguém poderia simplesmente desativar todos os

>

A estrutura de loop aninhado, o que garante a redutibilidade, é
praticamente jogado fora no início. [...] verifiquei a corrente
Implementações WebAssembly em JavaScriptCore, V8 e SpiderMonkey, e
todos parecem seguir esse padrão.

Não exatamente: pelo menos no SM, o gráfico IR não é um gráfico totalmente geral; nós
assuma certos invariantes de gráfico que se seguem de serem gerados a partir de um
fonte estruturada (JS ou wasm) e muitas vezes simplificam e/ou otimizam a
algoritmos.

O mesmo em V8. Na verdade, é uma das minhas principais queixas com a SSA em ambos
respectiva literatura e implementações que quase nunca definem
o que constitui um CFG "bem formado", mas tende a assumir implicitamente várias
restrições não documentadas de qualquer maneira, geralmente garantidas pela construção pelo
front-end da linguagem. Aposto que muitas/a maioria das otimizações em compiladores existentes
não seria capaz de lidar com CFGs verdadeiramente arbitrárias.

Como @lukewagner diz, o principal caso de uso para controle irredutível provavelmente é
"código encadeado" para intérpretes otimizados. Difícil dizer o quão relevantes esses
são para o domínio Wasm, e se sua ausência é realmente o maior
gargalo.

Tendo discutido o fluxo de controle irredutível com várias pessoas
pesquisando IRs do compilador, a solução "mais limpa" provavelmente seria adicionar
a noção de blocos mutuamente recursivos. Isso aconteceria para se encaixar no Wasm
estrutura de controle muito bem.

As otimizações de loop no LLVM geralmente ignoram o fluxo de controle irredutível e não tentam otimizá-lo. A análise de loop em que eles se baseiam reconhecerá apenas loops naturais, então você só precisa estar ciente de que pode haver ciclos CFG que não são reconhecidos como loops. Claro, outras otimizações são de natureza mais local e funcionam muito bem com CFGs irredutíveis.

De memória, e provavelmente errado, o SPEC2006 tem um único loop irredutível em 401.bzip2 e pronto. É bastante raro na prática.

O Clang emitirá apenas uma única instrução indirectbr em funções usando goto computado. Isso tem o efeito de transformar interpretadores encadeados em loops naturais com o bloco indirectbr como um cabeçalho de loop. Depois de sair do LLVM IR, o único indirectbr é duplicado no gerador de código para reconstruir o emaranhado original.

Não há algoritmo de verificação de passagem única para fluxo de controle irredutível
que estou ciente. A escolha do projeto apenas para fluxo de controle redutível foi
altamente influenciado por este requisito.

Como mencionado anteriormente, o fluxo de controle irredutível pode ser modelado pelo menos dois
jeitos diferentes. Um loop com uma instrução switch pode realmente ser otimizado
no gráfico irredutível original por um simples jump-threading local
otimização (por exemplo, dobrando o padrão onde uma atribuição de uma constante
para uma variável local ocorre, então uma ramificação para uma ramificação condicional que
liga imediatamente essa variável local).

Portanto, as construções de controle irredutíveis não são necessárias, e é
apenas uma questão de uma única transformação de back-end do compilador para recuperar o
gráfico irredutível original e otimizá-lo (para motores cujos compiladores
suporta fluxo de controle irredutível - o que nenhum dos 4 navegadores faz, para o
melhor do meu conhecimento).

melhor,
-Ben

Em quinta-feira, 20 de abril de 2017 às 5h20, Jakob Stoklund Olesen <
[email protected]> escreveu:

As otimizações de loop no LLVM geralmente ignoram o fluxo de controle irredutível
e não tentar otimizá-lo. A análise de loop em que eles se baseiam
só reconhece loops naturais, então você só precisa estar ciente de que pode
ser ciclos CFG que não são reconhecidos como loops. Claro, outros
otimizações são de natureza mais local e funcionam muito bem com irredutíveis
CFGs.

De memória, e provavelmente errado, SPEC2006 tem um único loop irredutível em
401.bzip2 e pronto. É bastante raro na prática.

O Clang emitirá apenas uma única instrução indiretabr em funções usando
calculado goto. Isso tem o efeito de transformar intérpretes encadeados em
loops naturais com o bloco indirectbr como cabeçalho de loop. Depois de sair
LLVM IR, o single indirectbr é duplicado no gerador de código
para reconstruir o emaranhado original.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

Também posso dizer que se construções irredutíveis fossem adicionadas a
WebAssembly, eles não funcionariam no TurboFan (JIT de otimização do V8), então
funções acabariam sendo interpretadas (extremamente lentas) ou sendo
compilado por um compilador de linha de base (um pouco mais lento), já que provavelmente não
investir esforços na atualização do TurboFan para suportar o fluxo de controle irredutível.
Isso significa que funções com fluxo de controle irredutível no WebAssembly
provavelmente acabar com um desempenho muito pior.

Claro, outra opção seria para o motor WebAssembly em V8 para executar o
relooper para alimentar gráficos redutíveis do TurboFan, mas isso tornaria a compilação
(e inicialização pior). Relooping deve permanecer um procedimento offline no meu
opinião, caso contrário, estamos terminando com custos inevitáveis ​​do motor.

melhor,
-Ben

Em segunda-feira, 1º de maio de 2017 às 12h48, Ben L. Titzer [email protected] escreveu:

Não há algoritmo de verificação de passagem única para controle irredutível
fluxo que eu conheço. A escolha de design apenas para fluxo de controle redutível
foi altamente influenciado por este requisito.

Como mencionado anteriormente, o fluxo de controle irredutível pode ser modelado pelo menos dois
jeitos diferentes. Um loop com uma instrução switch pode realmente ser otimizado
no gráfico irredutível original por um simples jump-threading local
otimização (por exemplo, dobrando o padrão onde uma atribuição de uma constante
para uma variável local ocorre, então uma ramificação para uma ramificação condicional que
liga imediatamente essa variável local).

Portanto, as construções de controle irredutíveis não são necessárias, e é
apenas uma questão de uma única transformação de back-end do compilador para recuperar o
gráfico irredutível original e otimizá-lo (para motores cujos compiladores
suporta fluxo de controle irredutível - o que nenhum dos 4 navegadores faz, para o
melhor do meu conhecimento).

melhor,
-Ben

Em quinta-feira, 20 de abril de 2017 às 5h20, Jakob Stoklund Olesen <
[email protected]> escreveu:

As otimizações de loop no LLVM geralmente ignoram o fluxo de controle irredutível
e não tentar otimizá-lo. A análise de loop em que eles se baseiam
só reconhece loops naturais, então você só precisa estar ciente de que pode
ser ciclos CFG que não são reconhecidos como loops. Claro, outros
otimizações são de natureza mais local e funcionam muito bem com irredutíveis
CFGs.

De memória, e provavelmente errado, o SPEC2006 tem um único loop irredutível
em 401.bzip2 e pronto. É bastante raro na prática.

O Clang emitirá apenas uma única instrução indiretabr em funções usando
calculado goto. Isso tem o efeito de transformar intérpretes encadeados em
loops naturais com o bloco indirectbr como cabeçalho de loop. Depois de sair
LLVM IR, o single indirectbr é duplicado no gerador de código
para reconstruir o emaranhado original.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

Existem métodos estabelecidos para verificação em tempo linear de fluxo de controle irredutível. Um exemplo notável é a JVM: com stackmaps, tem verificação em tempo linear. O WebAssembly já possui assinaturas de bloco em todas as construções semelhantes a blocos. Com informações de tipo explícito em cada ponto onde vários caminhos de fluxo de controle se fundem, não é necessário usar algoritmos de ponto fixo.

(Como um aparte, um tempo atrás eu perguntei por que alguém não permitiria que um hipotético operador pick lesse fora de seu bloco em profundidades arbitrárias. Esta é uma resposta: a menos que assinaturas sejam estendidas para descrever tudo um pick pode ler, a verificação de tipo de pick exigiria mais informações.)

O padrão loop-with-a-switch pode, é claro, ser desconectado, mas não é prático confiar nele. Se um mecanismo não o otimizar, ele terá um nível disruptivo de sobrecarga. Se a maioria dos mecanismos o otimiza, então não está claro o que é realizado mantendo o fluxo de controle irredutível fora da própria linguagem.

Suspiro... Eu queria responder mais cedo, mas a vida atrapalhou.

Estive procurando por alguns mecanismos JS e acho que tenho que enfraquecer minha afirmação sobre o fluxo de controle irredutível 'apenas funcionando'. Eu ainda não acho que seria tão difícil fazer isso funcionar, mas existem algumas construções que seriam difíceis de adaptar de uma maneira que realmente se beneficiaria…

Bem, vamos supor, para fins de argumentação, que fazer o pipeline de otimização suportar o fluxo de controle irredutível adequadamente é muito difícil. Um mecanismo JS ainda pode suportá-lo facilmente de maneira hacky, assim:

Dentro do backend, trate um bloco labels como se fosse um loop+switch até o último minuto. Em outras palavras, quando você vê um bloco labels , você o trata como um cabeçalho de loop com uma borda externa apontando para cada rótulo, e quando você vê um branch que tem como alvo um rótulo, você cria uma borda apontando para o cabeçalho labels , não para o rótulo de destino real - que deve ser armazenado separadamente em algum lugar. Não há necessidade de criar uma variável real para armazenar o rótulo de destino, como um loop+switch real teria que fazer; deve ser suficiente armazenar o valor em algum campo da instrução de desvio ou criar uma instrução de controle separada para esse propósito. Então, otimizações, agendamento e até mesmo alocação de registradores podem fingir que há dois saltos. Mas quando chega a hora de realmente gerar uma instrução de salto nativa, você verifica esse campo e gera um salto diretamente para o rótulo de destino.

Pode haver problemas com, por exemplo, qualquer otimização que mescla/exclui branches, mas deve ser bem fácil evitar isso; os detalhes dependem do projeto do motor.

De certa forma, minha sugestão é equivalente à “otimização simples de jump-threading local” do @titzer. Estou sugerindo fazer o fluxo de controle irredutível 'nativo' parecer um loop+switch, mas uma alternativa seria identificar loop+switches reais - ou seja, o padrão do @titzer onde ocorre uma atribuição de uma constante a uma variável local, então uma ramificação para uma ramificação condicional que ativa imediatamente essa variável local” – e adicione metadados permitindo que a ramificação indireta seja removida no final do pipeline. Se essa otimização se tornar onipresente, pode ser um substituto decente para uma instrução explícita.

De qualquer forma, a desvantagem óbvia da abordagem hacky é que as otimizações não entendem o gráfico de fluxo de controle real; eles efetivamente agem como se qualquer rótulo pudesse saltar para qualquer outro rótulo. Em particular, a alocação de registradores deve tratar uma variável como ativa em todos os rótulos, mesmo que, digamos, ela seja sempre atribuída logo antes de pular para um rótulo específico, como neste pseudocódigo:

a:
  control = 1;
  goto x;
b:
  control = 2;
  goto x;
...
x:
  // use control

Isso pode levar a um uso de registro seriamente abaixo do ideal em alguns casos. Mas, como observarei mais tarde, os algoritmos de vivacidade que os JITs usam podem ser fundamentalmente incapazes de fazer isso bem, de qualquer maneira…

Seja qual for o caso, otimizar tarde é muito melhor do que não otimizar nada. Um único salto direto é muito melhor do que um salto + comparação + carga + salto indireto; o preditor de ramificação da CPU pode eventualmente ser capaz de prever o destino do último com base no estado passado, mas não tão bem quanto o compilador. E você pode evitar gastar um registro e/ou memória na variável 'estado atual'.

Quanto à representação, qual é melhor: explícita (instrução labels ou similar) ou implícita (otimização de loop real+switches seguindo um padrão específico)?

Benefícios implícitos:

  • Mantém a especificação enxuta.

  • Pode já funcionar com o código de loop + switch existente. Mas eu não olhei para as coisas que o binaryen gera para ver se segue um padrão estrito o suficiente.

  • Fazer com que a maneira abençoada de expressar o fluxo de controle irredutível pareça um hack destaca o fato de que é mais lento em geral e deve ser evitado quando possível.

Desvantagens do implícito:

  • Parece um hack. É verdade que, como diz @titzer , isso não prejudica os mecanismos que 'adequadamente' suportam fluxo de controle irredutível; eles podem reconhecer o padrão antecipadamente e recuperar o fluxo irredutível original antes de realizar otimizações. Ainda assim, parece mais legal permitir apenas os saltos reais.

  • Cria um “cliff de otimização”, que o WebAssembly geralmente deve evitar em comparação com o JS. Relembrando, o padrão básico a ser otimizado é “onde ocorre uma atribuição de uma constante a uma variável local, depois uma ramificação para uma ramificação condicional que imediatamente liga essa variável local”. Mas e se, digamos, houver outras instruções no meio, ou a atribuição não estiver realmente usando uma instrução wasm const mas apenas algo conhecido como constante devido a otimizações? Alguns mecanismos podem ser mais liberais do que outros no que reconhecem como esse padrão, mas o código que tira vantagem disso (intencionalmente ou não) terá um desempenho muito diferente entre os navegadores. Ter uma codificação mais explícita define as expectativas com mais clareza.

  • Torna mais difícil usar wasm como um IR em etapas hipotéticas de pós-processamento. Se um compilador de alvo wasm faz as coisas da maneira normal e lida com todas as otimizações/transformações com um IR interno antes de eventualmente executar um relooper e, finalmente, gerar wasm, então não se importaria com a existência de sequências de instruções mágicas. Mas se um programa quiser executar qualquer transformação no próprio código wasm, ele teria que evitar quebrar essas sequências, o que seria irritante.

De qualquer forma, não me importo tanto assim - contanto que, se decidirmos sobre a abordagem implícita, os principais navegadores realmente se comprometam a realizar a otimização relevante.

Voltando à questão de suportar o fluxo irredutível nativamente - quais são os obstáculos, quanto benefício existe - aqui estão alguns exemplos específicos do IonMonkey de passes de otimização que teriam que ser modificados para suportá-lo:

AliasAnalysis.cpp: itera sobre blocos em pós-ordem reversa (uma vez) e gera dependências de ordenação para uma instrução (conforme usado em InstructionReordering) observando apenas os armazenamentos vistos anteriormente como possivelmente alias. Isso não funciona para fluxo de controle cíclico. Mas os loops (explicitamente marcados) são tratados especialmente, com uma segunda passagem que verifica as instruções nos loops em relação a qualquer armazenamento posterior em qualquer lugar do mesmo loop.

-> Então teria que haver alguma marcação de loop para blocos labels . Nesse caso, acho que marcar todo o bloco labels como um loop 'simplesmente funcionaria' (sem marcar especialmente os rótulos individuais), pois a análise é muito imprecisa para se preocupar com o fluxo de controle dentro do loop.

FlowAliasAnalysis.cpp: um algoritmo alternativo um pouco mais inteligente. Também itera sobre blocos em pós-ordem reversa, mas ao encontrar cada bloco ele mescla as informações de last-stores calculadas para cada um de seus predecessores (supõe-se que já tenham sido calculados), exceto para cabeçalhos de loop, onde leva em consideração o backedge.

-> Messier porque assume que (a) predecessores de blocos básicos individuais sempre aparecem antes dele, exceto para backedges de loop, e (b) um loop pode ter apenas um backedge. Existem diferentes maneiras de corrigir isso, mas provavelmente exigiria manipulação explícita de labels e, para que o algoritmo permanecesse linear, provavelmente teria que funcionar de maneira bastante grosseira nesse caso, mais como AliasAnalysis normal

BacktrackingAllocator.cpp: comportamento semelhante para alocação de registradores: ele faz uma passagem reversa linear pela lista de instruções e assume que todos os usos de uma instrução aparecerão após (ou seja, serão processados ​​antes) de sua definição, exceto quando encontrar backedges de loop: registradores que são live no início de um loop simplesmente permaneça ativo durante todo o loop.

-> Cada rótulo precisaria ser tratado como um cabeçalho de loop, mas a vivacidade teria que se estender por todo o bloco de rótulos. Não é difícil de implementar, mas, novamente, o resultado não seria melhor do que a abordagem hacky. Eu acho que.

@comex Outra consideração aqui é o quanto se espera que os mecanismos wasm façam. Por exemplo, você mencionou AliasAnalysis do Ion acima, mas o outro lado da história é que a análise de alias não é tão importante para o código WebAssembly, pelo menos por enquanto, enquanto a maioria dos programas está usando memória linear.

O algoritmo de animação BacktrackingAllocator.cpp da Ion exigiria algum trabalho, mas não seria proibitivo. A maior parte do Ion já lida com várias formas de fluxo de controle irredutível, pois o OSR pode criar várias entradas em loops.

Uma questão mais ampla aqui é o que os mecanismos de otimização do WebAssembly deverão fazer. Se alguém espera que o WebAssembly seja uma plataforma do tipo assembly, com desempenho previsível onde produtores/bibliotecas fazem a maior parte da otimização, então o fluxo de controle irredutível seria um custo bastante baixo porque os mecanismos não precisariam dos grandes algoritmos complexos onde é um fardo significativo . Se se espera que o WebAssembly seja um bytecode de nível superior, que faz mais otimização de alto nível automaticamente, e os mecanismos são mais complexos, torna-se mais valioso manter o fluxo de controle irredutível fora da linguagem, para evitar a complexidade extra.

BTW, também vale a pena mencionar nesta edição é o algoritmo de construção SSA on-the-fly de Braun et al , que é um

Estou interessado em usar o WebAssembly como um backend qemu no iOS, onde o WebKit (e o vinculador dinâmico, mas que verifica a assinatura de código) é o único programa que tem permissão para marcar a memória como executável. O codegen do Qemu assume que as instruções goto farão parte de qualquer processador para o qual ele tenha que codificar, o que torna um backend WebAssembly quase impossível sem que gotos sejam adicionados.

@tbodt - Você seria capaz de usar o relooper de Binaryen? Isso permite que você gere o que é basicamente Wasm-with-goto e depois o converte em fluxo de controle estruturado para Wasm.

@eholk Parece que seria muito mais lento do que uma tradução direta de código de máquina para wasm.

@tbodt Usar o Binaryen adiciona um IR extra no caminho, sim, mas não deve ser muito mais lento, acho, é otimizado para velocidade de compilação. E também pode ter outros benefícios além de lidar com gotos etc., pois você pode opcionalmente executar o otimizador Binaryen, que pode fazer coisas que o otimizador qemu não faz (coisas específicas do wasm).

Na verdade, eu estaria muito interessado em colaborar com você nisso, se você quiser :) Acho que portar o Qemu para o wasm seria muito útil.

Então, pensando bem, gotos não ajudariam muito. O codegen do Qemu gera o código para blocos básicos quando eles são executados pela primeira vez. Se um bloco salta para um bloco que ainda não foi gerado, ele gera o bloco e corrige o bloco anterior com um goto para o próximo bloco. O carregamento de código dinâmico e a correção de funções existentes não são coisas que podem ser feitas em webassembly, até onde eu sei.

@kripken Eu estaria interessado em colaborar, qual seria o melhor lugar para conversar com você?

Você não pode corrigir funções existentes diretamente, mas você pode usar call_indirect e o WebAssembly.Table para jit code. Para qualquer bloco básico que não foi gerado, você pode chamar o JavaScript, gerar o módulo WebAssembly e a instância de forma síncrona, extrair a função exportada e gravá-la sobre o índice na tabela. As chamadas futuras usarão sua função gerada.

Não tenho certeza de que alguém tenha tentado isso ainda, portanto, é provável que haja muitas arestas.

Isso poderia funcionar se tailcalls fossem implementados. Caso contrário, a pilha transbordaria rapidamente.

Outro desafio seria alocar espaço na tabela padrão. Como você mapeia um endereço para um índice de tabela?

Outra opção é regenerar a função wasm em cada novo bloco básico. Isso significa um número de recompilações igual ao número de blocos usados, mas eu aposto que é a única maneira de fazer o código rodar rapidamente depois de compilado (especialmente loops internos), e não precisa ser um recompilar, podemos reutilizar o Binaryen IR para cada bloco existente, adicionar IR para o novo bloco e apenas executar o relooper em todos eles.

(Mas talvez possamos fazer com que o qemu compile toda a função antecipadamente em vez de preguiçosamente?)

@tbodt para colaboração em fazer isso com Binaryen, uma opção é criar um repositório com seu trabalho (e pode usar problemas lá etc.), outra é abrir um problema específico no Binaryen para qemu.

Não podemos fazer com que o qemu compile uma função inteira de cada vez, porque o qemu não tem um conceito de "função".

Quanto a recompilar todo o cache de blocos, parece que pode levar muito tempo. Vou descobrir como usar o perfilador interno do qemu e, em seguida, abrir um problema no binaryen.

Pergunta lateral. Na minha opinião, uma linguagem direcionada ao WebAssembly deve ser capaz de fornecer uma função recursiva mutuamente eficiente. Para uma descrição de sua utilidade, convido você a ler: http://sharp-gamedev.blogspot.com/2011/08/forgotten-control-flow-construct.html

Em particular, a necessidade expressa por Cheery parece ser abordada pela função mutuamente recursiva.

Eu entendo a necessidade de recursão de cauda, ​​mas estou querendo saber se a função mutuamente recursiva só pode ser implementada se o maquinário subjacente fornecer gotos ou não. Se o fizerem, então para mim isso é um argumento legítimo a favor deles, pois haverá uma tonelada de linguagem de programação que terá dificuldade em direcionar o WebAssembly de outra forma. Se não o fizerem, talvez o mecanismo mínimo para suportar funções mutuamente recursivas seja tudo o que seria necessário (junto com a recursão da cauda).

@davidgrenier , as funções em um módulo Wasm são todas mutuamente recursivas. Você pode elaborar o que você considera ineficiente sobre eles? Você está se referindo apenas à falta de chamadas de cauda ou outra coisa?

Chamadas gerais de cauda estão chegando. A recursão da cauda (mútua ou não) será um caso especial disso.

Eu não estava dizendo que nada era ineficiente sobre eles. Estou dizendo que, se você os tiver, não precisará de goto geral porque funções mutuamente recursivas fornecem tudo o que o implementador de linguagem direcionado ao WebAssembly deve precisar.

Goto é muito útil para geração de código a partir de diagramas em programação visual. Talvez agora a programação visual não seja muito popular, mas no futuro pode atrair mais pessoas e acho que o wasm deve estar pronto para isso. Mais sobre a geração de código a partir dos diagramas e acesse: http://drakon-editor.sourceforge.net/generation.html

A próxima versão do Go 1.11 terá suporte experimental para WebAssembly. Isso incluirá suporte completo para todos os recursos do Go, incluindo goroutines, canais, etc. No entanto, o desempenho do WebAssembly gerado atualmente não é tão bom.

Isso se deve principalmente à falta da instrução goto. Sem a instrução goto, tivemos que recorrer ao uso de um loop de nível superior e uma tabela de salto em todas as funções. Usar o algoritmo relooper não é uma opção para nós, pois ao alternar entre goroutines precisamos poder retomar a execução em diferentes pontos de uma função. O relooper não pode ajudar com isso, apenas uma instrução goto pode.

É incrível que o WebAssembly tenha chegado ao ponto em que pode suportar uma linguagem como Go. Mas para ser verdadeiramente o assembly da web, o WebAssembly deve ser tão poderoso quanto outras linguagens assembly. Go possui um compilador avançado capaz de emitir assembly muito eficiente para várias outras plataformas. É por isso que eu gostaria de argumentar que é principalmente uma limitação do WebAssembly e não do compilador Go que não é possível usar também este compilador para emitir assembly eficiente para a web.

Usar o algoritmo relooper não é uma opção para nós, pois ao alternar entre goroutines precisamos poder retomar a execução em diferentes pontos de uma função.

Apenas para esclarecer, um goto regular não seria suficiente para isso, um goto computado é necessário para o seu caso de uso, correto?

Acho que um goto regular provavelmente seria suficiente em termos de desempenho. Saltos entre blocos básicos são estáticos de qualquer maneira e para trocar de goroutines um br_table com gotos em suas ramificações deve ter desempenho suficiente. O tamanho da saída é uma questão diferente.

Parece que você tem um fluxo de controle normal em cada função, mas também precisa da capacidade de pular da entrada da função para determinados outros locais no "meio", ao retomar uma goroutine - quantos desses locais existem? Se for cada bloco básico, o relooper será forçado a emitir um loop de nível superior pelo qual todas as instruções passam, mas se forem apenas algumas, isso não deve ser um problema. (Isso é realmente o que acontece com o suporte setjmp em emscripten - nós apenas criamos os caminhos extras necessários entre os blocos básicos do LLVM e deixamos o relooper processar isso normalmente.)

Cada chamada para alguma outra função é uma localização e a maioria dos blocos básicos tem pelo menos uma instrução de chamada. Estamos mais ou menos relaxando e restaurando a pilha de chamadas.

Eu vejo, obrigado. Sim, concordo que, para que isso seja prático, você precisa de goto estático ou suporte de restauração de pilha de chamadas (que também foi considerado).

Será possível chamar a função no estilo CPS ou implementar call/cc no WASM?

@Heimdell , o suporte para alguma forma de continuações delimitadas (também conhecido como "stack switching") está no roteiro, o que deve ser suficiente para quase qualquer abstração de controle interessante. Não podemos suportar continuações ilimitadas (ou seja, full call/cc), no entanto, uma vez que a pilha de chamadas Wasm pode ser arbitrariamente misturada com outros idiomas, incluindo chamadas reentrantes para o incorporador e, portanto, não pode ser considerada copiável ou móvel.

Lendo este tópico, tenho a impressão de que rótulos e gotos arbitrários têm um grande obstáculo antes de se tornarem um recurso:

  • O fluxo de controle não estruturado possibilita gráficos de fluxo de controle irredutíveis
  • Eliminando* qualquer "conversão rápida e simples, fácil de uma passagem para o formulário SSA"
  • Abrindo o compilador JIT para desempenho não linear
  • As pessoas que navegam em páginas da Web não devem sofrer atrasos se o compilador do idioma original puder fazer o trabalho inicial

_*embora possa haver alternativas como o algoritmo de construção SSA on-the-fly de Braun et al, que lida com fluxo de controle irredutível_

Se ainda estivermos presos lá, as chamadas _and_ tail estão avançando, talvez valha a pena pedir aos compiladores de linguagem que ainda traduzam para gotos, mas como etapa final antes da saída do WebAssembly, divida os "blocos de rótulo" em funções e converter os gotos em chamadas de cauda.

De acordo com o artigo de 1977 do designer de esquemas Guy Steele, Lambda: The Ultimate GOTO , a transformação deve ser possível, e o desempenho das chamadas de cauda deve ser capaz de corresponder de perto aos gotos.

Pensamentos?

Se ainda estivermos presos lá, as chamadas _and_ tail estão avançando, talvez valha a pena pedir aos compiladores de linguagem que ainda traduzam para gotos, mas como etapa final antes da saída do WebAssembly, divida os "blocos de rótulo" em funções e converter os gotos em chamadas de cauda.

Isso é essencialmente o que todo compilador faria de qualquer maneira, ninguém que eu conheça está defendendo gotos não gerenciados do tipo que causam tantos problemas na JVM, apenas para um gráfico de EBBs tipados. LLVM, GCC, Cranelift e todos os demais têm um CFG em formato SSA (possivelmente irredutível) como sua representação interna e os compiladores do Wasm ao nativo têm a mesma representação interna, então queremos preservar o máximo possível dessas informações e reconstruir o mínimo possível dessa informação. Os locais são com perdas, pois não são mais SSA, e o fluxo de controle do Wasm é com perdas, pois não é mais um CFG arbitrário. AFAIK tendo Wasm como uma máquina de registro SSA de registro infinito com informações de vivacidade de registro refinadas incorporadas provavelmente seria o melhor para codegen, mas o tamanho do código aumentaria, uma máquina de pilha com fluxo de controle modelado em um CFG arbitrário é provavelmente o melhor meio-termo . Eu posso estar errado sobre o tamanho do código com uma máquina de registro, porém, pode ser possível codificá-lo com eficiência.

A questão do fluxo de controle irredutível é que, se for irredutível no front-end, ainda é irredutível no wasm, a conversão de relooper/empilhador não torna o fluxo de controle redutível, apenas converte a irredutibilidade para ser dependente de valores de tempo de execução. Isso dá ao back-end menos informações e, portanto, pode produzir um código pior, a única maneira de produzir um bom código para CFGs irredutíveis agora é detectar os padrões emitidos pelo relooper e empilhador e convertê-los de volta para um CFG irredutível. A menos que você esteja desenvolvendo V8, que AFAIK suporta apenas fluxo de controle redutível, suportar fluxo de controle irredutível é puramente uma vitória - torna frontends e backends muito mais simples (frontends podem apenas emitir código no mesmo formato que armazenam internamente, backends não não precisa detectar padrões) enquanto produz uma saída melhor no caso de o fluxo de controle ser irredutível e uma saída tão boa ou melhor no caso usual em que o fluxo de controle é redutível.

Além disso, permitiria que GCC e Go começassem a produzir WebAssembly.

Eu sei que o V8 é um componente importante do ecossistema WebAssembly, mas parece ser a única parte desse ecossistema que se beneficia da situação atual do fluxo de controle, todos os outros back-ends que eu conheço convertem para um CFG de qualquer maneira e não são afetados por se o WebAssembly pode representar fluxo de controle irredutível ou não.

O v8 não poderia apenas incorporar o relooper para aceitar CFGs de entrada? Parece que grandes partes do ecossistema estão bloqueadas nos detalhes da implementação da v8.

Apenas para referência, notei que as instruções switch em c++ são muito lentas no wasm. Quando criei o perfil do código, tive que convertê-los para outras formas que operavam muito mais rapidamente para fazer o processamento de imagens. E nunca foi um problema em outras arquiteturas. Eu realmente gostaria de goto por razões de desempenho.

@graph , você poderia fornecer mais detalhes sobre como "instruções de comutação são lentas"? Sempre à procura de uma oportunidade para melhorar o desempenho... (Se você não quiser atrapalhar este tópico, envie-me um e-mail diretamente, [email protected].)

Vou postar aqui, pois isso se aplica a todos os navegadores. Declarações simples como esta quando compiladas com emscripten foram mais rápidas quando converti para instruções if.

for(y = ....) {
    for(x = ....) {
        switch(type){
        case IS_RGBA:....
         ....
        case IS_BGRA
        ....
        case IS_RGB
        ....
....

Eu suponho que o compilador estava convertendo uma tabela de salto para o que o wasm suporta. Eu não olhei em assembly gerado, então não posso confirmar.

Conheço algumas coisas não relacionadas a wasm que podem ser otimizadas para processamento de imagens na web. Já enviei via botão "feedback" no firefox. Se você estiver interessado, me avise e eu lhe enviarei as questões.

@graph Um benchmark completo seria muito útil aqui. Em geral, um switch em C pode se transformar em uma tabela de salto muito rápida no wasm, mas existem casos de canto que ainda não funcionam bem, que talvez precisemos corrigir, seja no LLVM ou nos navegadores.

Em emscripten especificamente, como os switches são tratados muda muito entre o backend fastcomp antigo e o novo upstream, então se você viu isso há algum tempo, ou recentemente, mas usando fastcomp, seria bom verificar no upstream.

@graph , Se o emscripten produzir uma br_table, o jit às vezes gerará uma tabela de salto e às vezes (se achar que será mais rápido) pesquisará o espaço de chaves linearmente ou com uma pesquisa binária em linha. O que ele faz geralmente depende do tamanho do switch. É claro que é possível que a política de seleção não seja a ideal... Concordo com @kripken , código executável seria muito útil aqui se você tiver algum para compartilhar.

(Não sei sobre v8 ou jsc, mas o Firefox atualmente não reconhece uma cadeia if-then-else como um possível switch, portanto, geralmente não é uma boa ideia abrir switches de código como cadeias if-then-else longas. ponto de equilíbrio provavelmente não passa de duas ou três comparações.)

@lars-t-hansen @kripken @graph pode muito bem ser que br_table esteja atualmente muito pouco otimizado, como esta troca parece mostrar: https://twitter.com/battagline/status/1168310096515883008

@aardappel , que curioso, os benchmarks que executei ontem não mostraram isso, no firefox no meu sistema o ponto de equilíbrio estava em torno de 5 casos que eu me lembro e depois disso br_table foi o vencedor. microbenchmark, é claro, e com alguma tentativa de distribuição uniforme das chaves de pesquisa. se o ninho "if" for enviesado para as chaves mais prováveis, de modo que não sejam necessários mais do que alguns testes, o ninho "if" vencerá.

Se não puder fazer a análise de intervalo no valor do switch para evitá-lo, o br_table também terá que fazer pelo menos um teste de filtragem para o intervalo do switch, o que também tira sua vantagem.

@lars-t-hansen Sim, não conhecemos seu caso de teste, pode ter um valor atípico. De qualquer forma, parece que o Chrome tem mais trabalho a fazer do que o Firefox.

Estou de férias, daí a minha falta de respostas. Obrigado pela compreensão.

@kripken @lars-t-hansen Eu executei alguns testes, parece que sim, era melhor agora no firefox. Ainda há alguns casos em que if-else supera o switch. Aqui está um caso:


Main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 3);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 4:
        switchSelect = SW4; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}

Dependendo do valor de switchSelect. if-else supera. Saída de exemplo:

Starting tests!
timing with SW = 32
switch time = 2.049000 seconds
accumulated value: 0
if-else time = 0.401000 seconds
accumulated value: 0

Como você pode ver para switchSelect = 32 if-else é muito mais rápido. Para os outros casos if-else é um pouco mais rápido. Para o caso switchSelect = 1 & 0, a instrução switch é mais rápida.

Test in Firefox 69.0.3 (64-bit)
compiled using: emcc -O3 -std=c++17 main.cpp -o main.html
emcc version: emcc (Emscripten gcc/clang-like replacement) 1.39.0 (commit e047fe4c1ecfae6ba471ca43f2f630b79516706b)

Usando o escripen estável mais recente em 20 de outubro de 2019. Nova instalação ./emcc activate latest .

Percebi acima que há um erro de digitação, mas isso não deve afetar o fato de o if-else ser o caso SW3 mais rápido, pois eles estão executando as mesmas instruções.

novamente com isso indo além do ponto de equilíbrio de 5: Interessante que para switchSelect=32 para este caso é semelhante em velocidade como if-else. Como você pode ver para 1003 if-else é um pouco mais rápido. Switch deve vencer neste caso.

Starting tests!
timing with SW = 1003
switch time = 2.253000 seconds
accumulated value: 1903939380
if-else time = 2.197000 seconds
accumulated value: 1903939380


main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 8);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;
    constexpr int SW5 = 64;
    constexpr int SW6 = 67;
    constexpr int SW7 = 1003;
    constexpr int SW8 = 256;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 3:
        switchSelect = SW4; break;
    case 4:
        switchSelect = SW5; break;
    case 5:
        switchSelect = SW6; break;
    case 6:
        switchSelect = SW7; break;
    case 7:
        switchSelect = SW8; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        case SW5:
            accumulator = (accumulator << 3) - accumulator + i; break;
        case SW6:
            accumulator = (i - accumulator) & 0xFF; break;
        case SW7:
            accumulator = i*i + accumulator; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
        else if(switchSelect == SW5)
            accumulator = (accumulator << 3) - accumulator + i;
        else if(switchSelect == SW6)
            accumulator = (i - accumulator) & 0xFF;
        else if(switchSelect == SW7)
            accumulator = i*i + accumulator;

    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}


Obrigado pessoal por dar uma olhada nesses casos de teste.

No entanto, esse é um switch muito escasso, que o LLVM deve converter para o equivalente a um conjunto de if-then's de qualquer maneira, mas aparentemente o faz de uma maneira menos eficiente do que o manual if-then. Você já tentou executar wasm2wat para ver como esses dois loops diferem no código?

Isso também depende fortemente deste teste usando o mesmo valor em cada iteração. Este teste seria melhor se percorresse todos os valores, ou melhor ainda, escolhido aleatoriamente deles (se isso puder ser feito de forma barata).

Melhor ainda, a verdadeira razão pela qual as pessoas usam o switch para desempenho é com um alcance denso, então você pode garantir que está realmente usando br_table por baixo. Ver em quantos casos br_table é mais rápido que if seria a coisa mais útil a saber.

O switch nos loops apertados foi usado porque era um código mais limpo em relação ao desempenho. Mas para o wasm, o impacto no desempenho era muito grande, então foi convertido em instruções if mais feias. Para processamento de imagem em muitos dos meus casos de uso, se eu quiser mais desempenho de um switch, eu moveria o switch para fora do loop e simplesmente teria cópias do loop para cada caso. Normalmente a troca é apenas alternar entre alguma forma de formato de pixel, formato de cor, codificação e etc... E em muitos casos as constantes são calculadas via define ou enums e não lineares. Vejo agora que meu problema não está relacionado ao projeto goto. Eu apenas tinha um entendimento incompleto sobre o que estava acontecendo para minhas instruções de troca. Espero que minhas notas sejam úteis para desenvolvedores de navegadores lendo isso para otimizar o wasm para processamento de imagens nesses casos. Obrigada.

Nunca pensei que goto pudesse ser um debate tão acalorado 😮 . Estou no barco de todo idioma deveria ter um goto 😁 . Outra razão para adicionar goto é reduzir a complexidade para o compilador compilar para wasm. Tenho certeza que isso é mencionado acima em algum lugar. Agora não tenho do que reclamar 😞 .

Mais algum progresso aí?

devido ao debate acalorado, eu diria que algum navegador adicionaria suporte para goto como uma extensão de bytecode não padrão. Então talvez o GCC possa entrar no jogo como suporte a uma versão não padrão. O que eu não acho bom no geral, mas permitirá mais competição de compiladores. Isso foi considerado?

Não houve muito progresso ultimamente, mas você pode querer dar uma olhada na proposta de funclets .

@graph para mim, sua sugestão soa como "vamos quebrar tudo e esperar o melhor".
Não funciona assim. Existem MUITOS benefícios da estrutura atual do WebAssembly (que não são óbvios, infelizmente). Tente mergulhar mais fundo na filosofia do wasm.

Permitir "Rótulos e Gotos arbitrários" nos trará de volta aos tempos (antigos) do bytecode não verificável. Todos os compiladores irão apenas mudar para uma "maneira preguiçosa" de fazer as coisas, em vez de "fazer certo".

É claro que o wasm em seu estado atual tem algumas omissões importantes. As pessoas estão trabalhando para preencher as lacunas (como a mencionada por @binji ), mas não acho que a "estrutura global do wasm" precise ser retrabalhada. Apenas minha humilde opinião.

@vshymanskyy A proposta de funclets, que fornece funcionalidade equivalente a rótulos e gotos arbitrários, é totalmente validável, em tempo linear.

Devo também mencionar que em nosso compilador Wasm de tempo linear, compilamos internamente todo o fluxo de controle Wasm em uma representação tipo funclets, sobre a qual tenho algumas informações neste post do bloco e a conversão do fluxo de controle Wasm para essa representação interna é implementada aqui . O compilador obtém todas as informações de tipo dessa representação tipo funclets, portanto, basta dizer que é trivial validar a segurança de tipo em tempo linear.

Acho que esse equívoco de que o fluxo de controle irredutível não pode ser validado em tempo linear vem da JVM, onde o fluxo de controle irredutível deve ser executado usando o interpretador em vez de ser compilado. Isso ocorre porque a JVM não tem como representar metadados de tipo para fluxo de controle irredutível e, portanto, não pode fazer a conversão de máquina de pilha para máquina de registro. "Gotos arbitrários" (ou seja, pular para byte/instrução X) não é verificável, mas separar uma função em blocos tipados, que podem ser saltados em uma ordem arbitrária, não é mais difícil de verificar do que separar um módulo em funções tipadas , que pode então ser saltado em uma ordem arbitrária. Você não precisa de gotos não tipados no estilo jump-to-byte-X para implementar quaisquer padrões úteis que seriam emitidos por compiladores como GCC e LLVM.

Eu simplesmente amo o processo aqui. O lado A explica por que isso é necessário em aplicações específicas. O lado B diz que está fazendo errado, mas não oferece suporte para esse aplicativo. O Lado A explica como nenhum dos argumentos pragmáticos de B se sustenta. O lado B não quer lidar com isso porque eles acham que o lado A está fazendo errado. O lado A está tentando atingir um objetivo. Lado B diz que esse é o objetivo errado, chamando-o de preguiçoso ou brutal. Os significados filosóficos mais profundos se perdem no lado A. O pragmático se perde no lado B, pois eles afirmam ter algum tipo de base moral mais elevada. O lado A vê isso como uma operação mecanicista amoral. Em última análise, o lado B geralmente permanece no controle da especificação para melhor ou para pior, e eles conseguiram uma quantidade incrível feita com sua pureza relativa.

Honestamente, eu apenas coloquei meu nariz aqui porque anos atrás, eu estava tentando fazer uma porta TinyCC para WASM para que eu pudesse executar um ambiente de desenvolvimento em um ESP8266 visando o ESP8266. Eu tenho apenas ~ 4 MB de armazenamento, portanto, incluir re-looper e mudar para um AST, bem como muitas outras alterações, está fora de questão. (Nota: Como o relooper é a única coisa como o relooper? É tão horrível e ninguém reescreveu esse otário em In C!?) Mesmo que fosse possível neste momento, não sei se escreveria um destino do TinyCC para o WASM, já que não é mais tão interessante para mim.

Este fio, no entanto. Caramba, esse tópico me trouxe tanta alegria existencial. Assistir a uma bifurcação na humanidade ser mais profunda do que democrata ou republicano, ou religião. Eu sinto que se isso pode ser resolvido. Se A pode vir a viver no mundo de B, ou B validar a afirmação de A de que a programação procedural tem seu lugar... Sinto que poderíamos resolver a paz mundial.

Alguém responsável pelo V8 poderia confirmar neste tópico que a oposição ao fluxo de controle irredutível não é influenciada pela implementação atual do V8?

Estou perguntando porque isso é o que mais me incomoda. Para mim, parece que isso deve ser uma discussão no nível de especificação sobre os prós e contras desse recurso. Ela não deve ser influenciada pela forma como uma implementação específica é projetada atualmente. No entanto, houve declarações que me fazem acreditar que a implementação do V8 está influenciando isso. Talvez eu esteja errado. Uma declaração aberta pode ajudar.

Bem, por mais que seja lamentável, as implementações atuais existentes até agora são tão importantes que o futuro (presumivelmente mais longo que o passado) não é tão importante. Eu estava tentando explicar isso em #1202, que a consistência é mais importante que as poucas implementações, mas parece que estou delirando. Boa sorte em explicar que algumas decisões de desenvolvimento em algum lugar em algum projeto não estão constituindo uma verdade universal, ou devem ser, por padrão, assumidas como corretas.

Este segmento é um canário na mina de carvão W3C. Embora eu tenha grande respeito por muitos indivíduos do W3C, a decisão de confiar o JavaScript à Ecma International, e não ao W3C, não foi tomada sem preconceitos.

Como @cnlohr , eu tinha esperanças de uma porta TCC wasm e por boas razões;

"O Wasm foi projetado como um destino de compilação portátil para linguagens de programação, permitindo a implantação na Web para aplicativos cliente e servidor." - webassembly.org

Claro, qualquer um pode pontificar por que goto é [INSERIR JARGÃO], mas que tal preferirmos padrões a opiniões. Todos podemos concordar que POSIX C é um bom alvo de linha de base, especialmente considerando que os langs de hoje são elaborados ou comparados com o C e o título da página inicial do WASM se apresenta como um alvo de compilação portátil para langs. Claro, alguns recursos serão mapeados como threads e simd. Mas, desconsiderar totalmente algo tão fundamental quanto goto , nem mesmo dar a decência de roteiro, não é consistente com o propósito declarado do WASM e tal postura do corpo de padronização que deu sinal verde para <marquee> está além do pálido.

De acordo com o padrão de codificação SEI CERT C Rec. "Considere usar uma cadeia goto ao deixar uma função em erro ao usar e liberar recursos" ;

Muitas funções requerem a alocação de vários recursos. Falhar e retornar em algum lugar no meio dessa função sem liberar todos os recursos alocados pode produzir um vazamento de memória. É um erro comum esquecer de liberar um (ou todos) os recursos dessa maneira, portanto, uma cadeia goto é a maneira mais simples e limpa de organizar as saídas, preservando a ordem dos recursos liberados.

A recomendação oferece um exemplo com a solução POSIX C preferida usando goto . Os pessimistas apontarão para a nota de que goto ainda é considerado prejudicial . Curiosamente, essa opinião não está incorporada em um desses padrões de codificação específicos, apenas uma nota. O que nos leva ao canário, o “considerado nocivo”.

Bottomline, uma consideração de "Regiões CSS" ou goto como prejudicial só deve ser ponderada juntamente com uma solução proposta para o problema para o qual tal recurso é usado. Se remover o referido recurso "prejudicial" equivale a remover os casos de uso razoáveis ​​sem alternativa, isso não é uma solução, na verdade é prejudicial aos usuários da linguagem.

Funções não são custo zero, mesmo em C. Se alguém oferecer um substituto para gotos & labels, canihaz por favor! Se alguém diz que eu não preciso disso, como eles sabem disso? Quando se trata de desempenho, goto pode nos dar aquele pequeno extra, difícil de argumentar com os engenheiros, de que não precisamos de recursos de alto desempenho e fáceis de entender que existem desde o início da linguagem.

Sem um plano para suportar goto , o WASM é um alvo de compilação de brinquedos, e tudo bem, talvez seja assim que o W3C vê a web. Espero que o WASM como padrão alcance mais alto, fora do espaço de endereço de 32 bits, e entre na corrida de compilação. Espero que o discurso da engenharia possa fugir do "isso não é possível..." para acelerar extensões C do GCC como Labels as Values porque o WASM deve ser INCRÍVEL. Pessoalmente, o TCC é consideravelmente mais impressionante e mais útil neste momento, sem todo o desperdício de pontificação, sem a página de destino hipster e o logotipo brilhante.

@d4tocchini :

De acordo com o padrão de codificação SEI CERT C Rec. "Considere usar uma cadeia goto ao deixar uma função em erro ao usar e liberar recursos" ;

Muitas funções requerem a alocação de vários recursos. Falhar e retornar em algum lugar no meio dessa função sem liberar todos os recursos alocados pode produzir um vazamento de memória. É um erro comum esquecer de liberar um (ou todos) os recursos dessa maneira, portanto, uma cadeia goto é a maneira mais simples e limpa de organizar as saídas, preservando a ordem dos recursos liberados.

A recomendação oferece um exemplo com a solução POSIX C preferida usando goto . Os pessimistas apontarão para a nota de que goto ainda é considerado prejudicial . Curiosamente, essa opinião não está incorporada em um desses padrões de codificação específicos, apenas uma nota. O que nos leva ao canário, o “considerado nocivo”.

O exemplo dado nessa recomendação pode ser expresso diretamente com quebras rotuladas, que estão disponíveis no Wasm. Ele não precisa do poder extra de goto arbitrário. (C não fornece break e continue rotulado, então tem que voltar para goto com mais frequência do que o necessário.)

@rossberg , bom ponto sobre quebras rotuladas nesse exemplo, mas discordo da sua suposição qualitativa de que C deve "recuar". goto é uma construção mais rica do que quebras rotuladas. Se C deve ser incluído entre os destinos de compilação portáteis e C não suporta quebras rotuladas, isso é um ponto mudo. Java rotulou break/continues enquanto Python rejeitou o recurso proposto , e considerando que tanto a sun JVM quanto o CPython padrão são escritos em C, você não concordaria que C como uma linguagem suportada deveria estar mais alta na lista de prioridades?

Se goto deve ser tão prontamente descartado de consideração, as centenas de usos de goto dentro da fonte de emscripten devem ser reconsideradas também?

Existe uma linguagem que não pode ser escrita em C? C como uma linguagem deve estar informando os recursos do WASM. Se o POSIX C não for possível com o WASM de hoje, aí está o seu roteiro adequado.

Não exatamente no tópico do argumento, mas para não colocar sombra de que os erros aleatórios estão à espreita aqui e ali na argumentação em geral:

Python rotulou quebras

Você pode elaborar? (Aka: Python não tem quebras rotuladas.)

@pfalcon , sim, foi mal, editei meu comentário para esclarecer que o python propôs quebras/continuações rotuladas e o rejeitei

Se goto deve ser tão prontamente descartado, as centenas de usos de goto na fonte de emscripten devem ser reconsideradas também?

1) Observe quanto disso está presente no musl libc, não diretamente no emscripten. (O segundo mais usado é tests/third_party)
2) Construções de nível de fonte não são as mesmas que instruções de bytecode
3) Emscripten não está no mesmo nível de abstração que o padrão wasm, então, não, não deve ser reconsiderado com base nisso.

Especificamente, pode ser útil hoje reescrever os gotos da libc, porque assim teríamos mais controle sobre o cfg resultante do que confiar em relooper/cfgstackify para lidar bem com ele. Nós não temos porque é uma quantidade não trivial de trabalho para acabar com um código totalmente divergente do musl upstream.

Os desenvolvedores do Emscripten (a última vez que verifiquei) tendem a ser da opinião de que uma estrutura semelhante a goto seria muito boa, por essas razões óbvias, portanto, é improvável que a deixe de considerar, mesmo que leve anos para chegar a um compromisso aceitável.

tal postura do corpo de padronização que dá sinal verde para <marquee> está além dos limites.

Esta é uma declaração particularmente estúpida.

1) Nós, a Internet mais ampla, estamos a mais de uma década de tomar essa decisão
2) We-the-wasm-CG somos um grupo totalmente (quase?) separado de pessoas dessa tag, e provavelmente também se incomodam individualmente com erros óbvios do passado.

sem todo o pontificado desperdiçado, sem a página de destino hipster e o logotipo brilhante.

Isso poderia ter sido reformulado para "Estou frustrado" sem problemas de tom.

Como este tópico mostra, essas conversas são difíceis o suficiente.

Há um novo nível de preocupação profunda quando você deseja reescrever um conjunto de funções profundamente confiável e compreendido para todos os novos apenas porque um ambiente para seu uso precisa passar por etapas extras para suportá-lo. (embora eu ainda esteja no campo firmemente, por favor, adicione-goto porque odeio estar preso a usar apenas um compilador específico)

Eu acho que esse tópico deixou de ser produtivo - ele está em execução há mais de quatro anos e parece que todos os argumentos possíveis a favor e contra goto s arbitrários foram usados ​​aqui; deve-se notar também que nenhum desses argumentos é particularmente novo;)

Existem runtimes gerenciados que optaram por não ter rótulos de salto arbitrários, o que funcionou bem para eles. Além disso, existem sistemas de programação onde saltos arbitrários são permitidos e estão indo bem também. No final, os autores de um sistema de programação fazem escolhas de design e só o tempo realmente mostra se essas escolhas são bem-sucedidas ou não.

As escolhas de design do Wasm que proíbem saltos arbitrários são essenciais para sua filosofia. É improvável que possa suportar goto s sem algo como funclets, pelas mesmas razões que não suporta saltos indiretos puros.

As escolhas de design do Wasm que proíbem saltos arbitrários são essenciais para sua filosofia. É improvável que ele possa suportar gotos sem algo como funclets, pelas mesmas razões que não suporta saltos indiretos puros.

@penzn Por que a proposta de funclets está

Se estivéssemos discutindo um projeto de código aberto comum, eu faria um fork e terminaria com ele. Estamos falando de um padrão de monopólio de longo alcance aqui. A resposta vigorosa da comunidade deve ser cultivada porque nos importamos.

@J0eCool

  1. Observe quanto disso está presente em musl libc, não diretamente em emscripten. (O segundo mais usado é tests/third_party)

Sim, o aceno foi para o quanto é usado em C em geral.

  1. Construções de nível de origem não são as mesmas que instruções de bytecode

Claro, o que estamos discutindo é uma preocupação interna que afeta as construções de nível de origem. Isso é parte da frustração, a caixa preta não deve vazar suas preocupações.

  1. Emscripten não está no mesmo nível de abstração que o padrão wasm, portanto, não deve ser reconsiderado com base nisso.

O ponto é que você encontrará goto s na maioria dos projetos C consideráveis, mesmo dentro da cadeia de ferramentas WebAssembly em geral. Um alvo de compilador portátil para linguagens em geral que não é expressivo o suficiente para direcionar seus próprios compiladores não é exatamente consistente com a natureza de nossa empresa.

Especificamente, pode ser útil hoje reescrever os gotos da libc, porque assim teríamos mais controle sobre o cfg resultante do que confiar em relooper/cfgstackify para lidar bem com ele.

Isso é circular. Muitos acima levantaram sérias questões não respondidas sobre a infalibilidade de tal exigência.

Nós não temos porque é uma quantidade não trivial de trabalho para acabar com um código totalmente divergente do musl upstream.

É possível remover gotos, como você disse, é uma quantidade de trabalho não trivial ! Você está sugerindo que todos os outros devem divergir descontroladamente os caminhos de código porque os gotos não devem ser suportados?

Os desenvolvedores do Emscripten (a última vez que verifiquei) tendem a ser da opinião de que uma estrutura semelhante a goto seria muito boa, por essas razões óbvias, portanto, é improvável que a deixe de considerar, mesmo que leve anos para chegar a um compromisso aceitável.

Um vislumbre de esperança! Eu ficaria satisfeito se o suporte goto/label fosse levado a sério com um item de roteiro + convite oficial para colocar a bola em movimento, mesmo que daqui a alguns anos.

Esta é uma declaração particularmente estúpida.

Você tem razão. Perdoe a hipérbole, estou um pouco frustrado. Eu amo o wasm e o uso com frequência, mas no final vejo uma estrada de dor à minha frente se quiser fazer algo digno de nota com ele, como o port TCC. Depois de ler todos os comentários e artigos, ainda não consigo descobrir se a oposição é técnica, filosófica ou política. Como @neelance expressou,

“Alguém responsável pelo V8 poderia confirmar neste tópico que a oposição ao fluxo de controle irredutível não é influenciada pela implementação atual do V8?

Estou perguntando porque isso é o que mais me incomoda. [...]

Se vocês ouvirem alguma coisa útil, levem a sério o feedback de @neelance sobre o Go 1.11. Isso é difícil de argumentar. Claro, todos nós podemos fazer a varredura não trivial de goto, mas mesmo assim, levamos um sério golpe de perf que só pode ser corrigido com uma instrução goto.

Mais uma vez, perdoe minha frustração, mas se este problema for encerrado sem o devido endereço, temo que enviará o tipo errado de sinal que apenas exasperará esse tipo de resposta da comunidade e é inadequado para um dos maiores esforços de padrões de nossa campo. Escusado será dizer que sou um grande fã e torcedor de todos nesta equipe. Obrigada!

Aqui está outro problema do mundo real causado pela falta de goto/funclets: https://github.com/golang/go/issues/42979

Para este programa, o compilador Go atualmente gera um binário wasm com 18.000 block s aninhados. O binário wasm em si tem um tamanho de 2,7 MB, mas quando eu o executo em wasm2wat recebo um arquivo .wat de 4,7 GB. 🤯

Eu poderia tentar dar ao compilador Go alguma heurística para que, em vez de uma única tabela de salto enorme, ele pudesse criar algum tipo de árvore binária e, em seguida, examinar a variável de destino do salto várias vezes. Mas é realmente assim que deveria ser com wasm?

Eu gostaria de acrescentar que acho estranho como as pessoas parecem pensar que está perfeitamente bem se apenas um único compilador (Emscripten[1]) puder suportar de forma realista o WebAssembly.
Me lembra um pouco a situação do libopus (um padrão que normativamente depende do código protegido por direitos autorais).

Também acho estranho como os desenvolvedores do WebAssembly parecem ser tão veementemente contra isso, apesar de quase todos do lado do compilador dizerem que é necessário. Lembre-se: WebAssembly é um padrão, não um manifesto. E o fato é que a maioria dos compiladores modernos usa alguma forma de SSA + blocos básicos internamente (ou algo quase equivalente, com as mesmas propriedades), que não possuem o conceito de loops explícitos[2]. Até os JITs usam algo parecido, isso é comum.
O requisito absoluto para que o reloop aconteça sem a saída de "apenas use goto" é, que eu saiba[3], sem precedentes fora dos tradutores de idioma para idioma --- e mesmo assim, apenas tradutores de idioma para idioma que segmentar idiomas sem goto. Em particular, nunca ouvi falar que isso precisa ser feito para qualquer tipo de IR ou bytecode, exceto WebAssembly.

Talvez seja hora de renomear WebAssembly para WebEmscripten (WebScripten?).

Como o @d4tocchini disse, se não fosse pelo status monopolista do WebAssembly (necessário, devido à situação de padronização), ele provavelmente já teria sido bifurcado em algo que pode suportar razoavelmente o que os desenvolvedores do compilador já sabem que ele precisa suportar.
E não, "apenas use emscripten" não é um contra-argumento válido, porque faz com que o padrão dependa de um único fornecedor de compilador. Espero não precisar dizer por que isso é ruim.

EDIT: esqueci de adicionar uma coisa:
Você ainda não esclareceu se a questão é técnica, filosófica ou política. Eu suspeito do último, mas ficaria feliz em ser provado errado (porque questões técnicas e filosóficas podem ser corrigidas muito mais facilmente do que políticas).

Aqui está outro problema do mundo real causado pela falta de goto/funclets: golang/go#42979

Para este programa, o compilador Go atualmente gera um binário wasm com 18.000 block s aninhados. O binário wasm em si tem um tamanho de 2,7 MB, mas quando eu o executo em wasm2wat recebo um arquivo .wat de 4,7 GB. 🤯

Eu poderia tentar dar ao compilador Go alguma heurística para que, em vez de uma única tabela de salto enorme, ele pudesse criar algum tipo de árvore binária e, em seguida, examinar a variável de destino do salto várias vezes. Mas é realmente assim que deveria ser com wasm?

Este exemplo é realmente interessante. Como um programa de linha reta tão simples gera esse código? Qual é a relação entre o número de elementos da matriz e o número de blocos? Em particular, devo interpretar isso como significando que cada acesso ao elemento da matriz requer que _multiple_ blocos sejam compilados fielmente?

E não, "apenas use emscripten" não é um contra-argumento válido

Eu acho que o verdadeiro contra-argumento nesse sentido seria que outro compilador que deseja direcionar o Wasm pode/deve implementar seu próprio algoritmo semelhante a um relooper. Pessoalmente, acho que o Wasm deve eventualmente ter um loop de vários corpos (próximo aos funclets) ou algo semelhante que seja um alvo natural para goto .

@conrad-watt Existem vários fatores que fazem com que cada atribuição use vários blocos básicos no CFG. Uma delas é que há uma verificação de comprimento na fatia porque o comprimento não é conhecido em tempo de compilação. Geralmente eu diria que os compiladores consideram os blocos básicos como uma construção relativamente barata, mas com wasm eles são um pouco caros, especialmente neste caso em particular.

@neelance no exemplo modificado em que o código é dividido entre várias funções, a sobrecarga de memória (tempo de execução/compilação) é muito menor. São menos blocos gerados neste caso, ou é apenas que as funções separadas significam que o GC do mecanismo pode ser mais granular?

@conrad-watt Não é nem o código Go que está usando a memória, mas o host WebAssembly: Quando eu instanciar o binário wasm com o Chrome 86, minha CPU vai para 100% por 2 minutos e o uso de memória da guia atinge o pico em 11,3 GB. Isso é antes que o código wasm binário / Go seja executado. É a forma do binário wasm que está causando o problema.

Esse já era meu entendimento. Eu esperaria que um grande número de anotações de blocos/tipo causassem sobrecarga de memória especificamente durante a compilação/instanciação.

Para tentar desambiguar minha pergunta anterior - se a versão dividida do código compilar para Wasm com menos blocos (por causa de alguma peculiaridade do relooper), isso seria uma explicação para a sobrecarga de memória reduzida e seria uma boa motivação para adicionar mais geral controle de fluxo para Wasm.

Alternativamente, pode ser que o código dividido resulte em (aproximadamente) o mesmo número total de blocos, mas como cada função é compilada separadamente por JIT, os metadados/IR usados ​​para compilar cada função podem ser mais rapidamente GC'd pelo mecanismo Wasm . Um problema semelhante ocorreu na V8 anos atrás ao analisar/compilar funções asm.js grandes. Nesse caso, a introdução de um fluxo de controle mais geral no Wasm não resolveria o problema.

Primeiro, gostaria de esclarecer: o compilador Go não está usando o algoritmo relooper, porque é inerentemente incompatível com o conceito de comutação de goroutines. Todos os blocos básicos são expressos por meio de uma tabela de saltos com um pouco de queda sempre que possível.

Acho que há um crescimento exponencial de complexidade no tempo de execução wasm do Chrome em relação à profundidade de block s aninhados. A versão dividida tem o mesmo número de blocos, mas uma profundidade máxima menor.

Nesse caso, a introdução de um fluxo de controle mais geral no Wasm não resolveria o problema.

Concordo que esse problema de complexidade provavelmente pode ser resolvido no final do Chrome. Mas eu sempre gosto de fazer a pergunta "Por que esse problema existe em primeiro lugar?". Eu diria que com um fluxo de controle mais geral, esse problema nunca teria existido. Além disso, ainda há a sobrecarga de desempenho geral significativa devido a todos os blocos básicos serem expressos como tabelas de salto, que acho improvável que desapareçam pela otimização.

Acho que há um crescimento exponencial de complexidade no tempo de execução wasm do Chrome em relação à profundidade dos blocos aninhados. A versão dividida tem o mesmo número de blocos, mas uma profundidade máxima menor.

Isso significa que em uma função de linha reta com N acessos ao array, o acesso final ao array será aninhado (algum fator constante de) N blocos de profundidade? Em caso afirmativo, existe uma maneira de reduzir isso fatorando o código de tratamento de erros de maneira diferente? Eu esperaria que qualquer compilador chug se tivesse que analisar 3000 loops aninhados (analogia muito grosseira), portanto, se isso for inevitável por razões semânticas, isso também seria um argumento para um fluxo de controle mais geral.

Se a diferença de aninhamento for menos gritante do que isso, meu palpite seria que o V8 quase não faz GC'ing de metadados _durante_ compilação de uma única função Wasm, então mesmo se tivéssemos algo como uma proposta de funclets ajustada na linguagem desde o início , as mesmas despesas gerais ainda seriam visíveis sem que eles fizessem alguma otimização de GC interessante.

Além disso, ainda há a sobrecarga de desempenho geral significativa devido a todos os blocos básicos serem expressos como tabelas de salto, que acho improvável que desapareçam pela otimização.

Concordo que é claramente preferível (de um ponto de vista puramente técnico) ter um alvo mais natural aqui.

Isso significa que em uma função de linha reta com N acessos ao array, o acesso final ao array será aninhado (algum fator constante de) N blocos de profundidade? Em caso afirmativo, existe uma maneira de reduzir isso fatorando o código de tratamento de erros de maneira diferente? Eu esperaria que qualquer compilador chug se tivesse que analisar 3000 loops aninhados (analogia muito grosseira), portanto, se isso for inevitável por razões semânticas, isso também seria um argumento para um fluxo de controle mais geral.

O contrário: a primeira atribuição é aninhada tão profundamente, não a última. block s aninhados e um único br_table no topo é como uma instrução tradicional switch é expressa em wasm. Esta é a tabela de salto que mencionei. Não há 3.000 loops aninhados.

Se a diferença de aninhamento for menos gritante do que isso, meu palpite seria que o V8 quase não faz GC'ing de metadados durante a compilação de uma única função Wasm, então mesmo se tivéssemos algo como uma proposta de funclets ajustada na linguagem desde o início , as mesmas despesas gerais ainda seriam visíveis sem que eles fizessem alguma otimização de GC interessante.

Sim, também pode haver alguma implementação que tenha complexidade exponencial em relação ao número de blocos básicos. Mas lidar com blocos básicos (mesmo em grande quantidade) é o que muitos compiladores fazem o dia todo. Por exemplo, o próprio compilador Go lida com esse número de blocos básicos facilmente durante sua compilação, mesmo que sejam processados ​​por vários passos de otimização.

Sim, também pode haver alguma implementação que tenha complexidade exponencial em relação ao número de blocos básicos. Mas lidar com blocos básicos (mesmo em grande quantidade) é o que muitos compiladores fazem o dia todo. Por exemplo, o próprio compilador Go lida com esse número de blocos básicos facilmente durante sua compilação, mesmo que sejam processados ​​por vários passos de otimização.

Claro, mas um problema de desempenho aqui seria ortogonal a como o fluxo de controle entre esses blocos básicos é expresso no idioma original (ou seja, não uma motivação para um fluxo de controle mais geral no Wasm). Para ver se o V8 é particularmente ruim aqui, pode-se verificar se FireFox/SpiderMonkey ou Lucet/Cranelift exibem os mesmos overheads de compilação.

Eu fiz mais alguns testes: Firefox e Safari não mostram nenhum problema. Curiosamente, o Chrome é capaz de executar o código antes que o processo intensivo termine, então parece que alguma tarefa não estritamente necessária para executar o binário wasm está tendo o problema de complexidade.

Claro, mas um problema de desempenho aqui seria ortogonal à forma como o fluxo de controle entre esses blocos básicos é expresso no idioma de origem original.

Eu vejo o seu ponto.

Eu ainda acredito que representar blocos básicos não por meio de instruções de salto, mas por meio de uma variável de salto e uma enorme tabela de salto / blocos aninhados está expressando o conceito simples de blocos básicos de uma maneira bastante complexa. Isso leva à sobrecarga de desempenho e ao risco de problemas de complexidade, como o que vimos aqui. Acredito que sistemas mais simples são melhores e mais robustos do que sistemas complexos. Ainda não vi argumentos que me convençam de que o sistema mais simples é uma má escolha. Só ouvi dizer que o V8 teria dificuldade em implementar o fluxo de controle arbitrário e minha pergunta aberta para me dizer que esta declaração está errada (https://github.com/WebAssembly/design/issues/796#issuecomment-623431527) não foi respondido ainda.

@neelance

O Chrome pode até executar o código antes que o processo intensivo termine

Parece que o compilador de linha de base Liftoff está ok, e o problema está no compilador de otimização TurboFan. Registre um problema ou forneça um caso de teste e eu posso registrar um, se você preferir.

De forma mais geral: você acha que os planos de troca de pilha wasm serão capazes de resolver os problemas de implementação de goroutine do Go? Esse é o melhor link que posso encontrar, mas está bastante ativo agora, com uma reunião quinzenal e vários casos de uso fortes que motivam o trabalho. Se Go pode usar corrotinas wasm para evitar o padrão de troca grande, acho que gotos arbitrários não seriam necessários.

O compilador Go não está usando o algoritmo relooper, porque é inerentemente incompatível com o conceito de comutação de goroutines.

É verdade que não pode ser aplicado por si só. No entanto, temos bons resultados com o uso de fluxo de controle estruturado wasm +

Eu ficaria muito feliz em experimentar isso em Go, se você estiver interessado! Isso obviamente não seria tão bom quanto o suporte de troca de pilha embutido no wasm, mas poderia ser melhor do que o padrão de troca grande já. E seria mais fácil mudar para o suporte de comutação de pilha integrado posteriormente. Concretamente, como esse experimento poderia funcionar é fazer o Go emitir código normalmente estruturado, sem se preocupar com a troca de pilha, e apenas emitir uma chamada para uma função especial maybe_switch_goroutine em pontos apropriados. A transformação Asyncify cuidaria de todo o resto basicamente.

Estou interessado em gotos para emuladores de recompilação dinâmica, como o qemu. Ao contrário de outros compiladores, o qemu em nenhum momento tem conhecimento da estrutura do fluxo de controle do programa e, portanto, os gotos são o único alvo razoável. Tailcalls podem resolver isso, compilando cada bloco como uma função e cada goto como um tailcall.

@kripken Obrigado pelo seu post muito útil.

Parece que o compilador de linha de base Liftoff está ok, e o problema está no compilador de otimização TurboFan. Registre um problema ou forneça um caso de teste e eu posso registrar um, se você preferir.

Aqui está um binário wasm que você pode executar com wasm_exec.html .

Você acha que os planos de troca de pilha wasm serão capazes de resolver os problemas de implementação de goroutine do Go?

Sim, à primeira vista, parece que isso ajudaria.

No entanto, temos bons resultados com o uso de fluxo de controle estruturado wasm + Asyncify.

Isso parece promissor também. Precisaríamos implementar o relooper em Go, mas tudo bem, eu acho. Uma pequena desvantagem é que ele adiciona uma dependência ao binaryen para produzir binários wasm. Provavelmente escreverei uma proposta em breve.

Eu acredito que o algoritmo de empilhador do LLVM é mais fácil/melhor, caso você queira implementar isso: https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2

Apresentei uma proposta para o projeto Go: https://github.com/golang/go/issues/43033

@neelance , bom ver que a sugestão de @kripken ajuda um pouco com golang + wasm. Considerando que esse problema é um de goto/labels não de comutação de pilha, e dado que o Asyncify introduz novas compilações de deps/casing especiais com o Asyncify até que a comutação de pilha seja lançada, etc - você caracterizaria isso como uma solução ou menos que a mitigação ideal? Como isso se compara aos benefícios estimados se as instruções goto estivessem disponíveis?

Se o argumento “Bom Gosto” de Linus Torvalds para listas encadeadas se baseia na elegância de remover uma única declaração de ramificação em caixa especial, é difícil ver esse tipo de ginástica de caixa especial como uma vitória ou mesmo um passo na direção certa. Tendo usado pessoalmente gotos para APIs assíncronas em C, para falar sobre troca de pilha antes que as instruções goto acionem todos os tipos de cheiros.

Por favor, corrija-me se eu estiver interpretando errado, mas além de respostas aparentemente flutuantes focadas em particularidades marginais para algumas questões levantadas, parece que os mantenedores aqui não ofereceram nenhuma clareza sobre o assunto em questão nem responderam às perguntas difíceis. Com todo o respeito, essa lenta ossificação não é a marca registrada da política corporativa calo? Se este for o caso, eu entendo a situação... Imagine todas as linguagens/compiladores que a marca Wasm poderia oferecer suporte se apenas o ANSI C fosse um teste decisivo de compatibilidade!

@neelance @darkuranium @d4tocchini nem todos os contribuidores do Wasm acham que a falta de goto é a coisa certa, na verdade, eu pessoalmente classificaria isso como o erro de design nº 1 do Wasm. Sou absolutamente a favor de adicioná-lo (como funclets ou diretamente).

No entanto, debater neste tópico não fará com que os gotos aconteçam, e nem magicamente fará com que todos os envolvidos em Wasm se convençam e façam o trabalho por você. Aqui estão os passos a tomar:

  1. Junte-se ao Wasm CG.
  2. Alguém investe tempo para se tornar campeão de uma proposta de goto. Eu recomendo começar com a proposta de funclets existente, pois já foi bem pensada pelo @sunfishcode para ser o "menos intrusivo" para os mecanismos e ferramentas atuais que dependem da estrutura de blocos, portanto, tem uma chance maior de sucesso do que um raw vamos para.
  3. Ajude-o a passar pelas 4 etapas da proposta. Isso inclui fazer bons designs para quaisquer objeções que surjam em seu caminho, iniciar discussões, com o objetivo de deixar um número suficiente de pessoas felizes, de modo que você obtenha votos da maioria ao avançar pelas etapas.

@d4tocchini Honestamente, atualmente vejo as soluções sugeridas como "o melhor caminho a seguir, dadas as circunstâncias que não posso mudar", também conhecida como "solução alternativa". Eu ainda considero as instruções jump/goto (ou funclets) como a maneira mais simples e, portanto, preferível. (Ainda obrigado a @kripken por sugerir as alternativas de maneira útil.)

@aardappel Até onde eu sei, @sunfishcode tentou empurrar a proposta de funclets e falhou. Por que seria diferente para mim?

@neelance Eu não acho que a @sunfishcode teve muito tempo para levar a proposta além de sua criação inicial, então ela está "parada" em vez de "falhou". Como eu estava tentando indicar, é necessário que um campeão faça um trabalho contínuo para que uma proposta chegue até o final do pipeline.

@neelance

Obrigado pelo teste! Posso confirmar o mesmo problema localmente. Eu arquivei https://bugs.chromium.org/p/v8/issues/detail?id=11237

Precisaríamos implementar o relooper em Go [..] Uma pequena desvantagem é que ele adiciona uma dependência ao binaryen para produzir binários wasm.

Btw, se isso ajudar, podemos fazer uma compilação de biblioteca de binaryen como um único arquivo C. Talvez isso seja mais fácil de integrar?

Além disso, usando o Binaryen, você pode usar a implementação do Relooper que está lá . Você pode passar blocos básicos de IR e deixá-lo fazer o reloop.

@taralx

Eu acredito que o algoritmo de empilhador do LLVM é mais fácil/melhor,

Observe que esse link não é sobre o LLVM upstream, é o compilador Cheerp (que é um fork do LLVM). Seu Stackifier tem um nome semelhante ao do LLVM, mas é diferente.

Observe também que esse post do Cheerp se refere ao algoritmo original de 2011 - a implementação do relooper moderno (como mencionado anteriormente) não teve os problemas mencionados por muitos anos. Não conheço uma alternativa mais simples ou melhor para essa abordagem geral, que é muito semelhante ao que Cheerp e outros fazem - são variações de um tema.

@kripken Obrigado por registrar o problema.

Btw, se isso ajudar, podemos fazer uma compilação de biblioteca de binaryen como um único arquivo C. Talvez isso seja mais fácil de integrar?

Improvável. O próprio compilador Go foi convertido para Go puro há algum tempo e afaik não usa outras dependências C. Eu não acho que isso será uma exceção.

Aqui está o estado atual da proposta de funclets: O próximo passo no processo é pedir uma votação do CG para entrar no estágio 1.

Eu mesmo estou atualmente focado em outras áreas do WebAssembly e não tenho largura de banda para empurrar funclets adiante; se alguém estiver interessado em assumir o papel de Campeão para funclets, eu ficaria feliz em entregá-lo.

Improvável. O próprio compilador Go foi convertido para Go puro há algum tempo e afaik não usa outras dependências C. Eu não acho que isso será uma exceção.

Além disso, isso não resolve o problema do uso extensivo de relooper causando sérios problemas de desempenho nos tempos de execução do WebAssembly.

@Vurich

Eu acho que esse poderia ser o melhor caso para adicionar gotos ao wasm, mas alguém precisaria coletar dados convincentes do código do mundo real mostrando esses penhascos de desempenho tão sérios. Eu mesmo não vi esses dados. Trabalho analisando déficits de desempenho wasm como "Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code" (2019) também não suporta o fluxo de controle sendo um fator significativo (eles observam uma quantidade maior de instruções não são devido ao fluxo de controle estruturado - em vez disso, é devido a verificações de segurança).

@kripken Você tem alguma sugestão sobre como coletar esses dados? Como se mostraria que um déficit de desempenho é devido ao fluxo de controle estruturado?

Improvável que haja muito trabalho analisando o desempenho da etapa de compilação, que faz parte da reclamação aqui.

Estou um pouco surpreso que ainda não tenhamos uma construção de switch case, mas os funclets incluem isso.

@neelance

Não é fácil descobrir as causas específicas, sim. Para, por exemplo, verificações de limites, você pode simplesmente desativá-los na VM e medir isso, mas não há uma maneira simples de fazer o mesmo para gotos, infelizmente.

Uma opção é comparar manualmente o código de máquina emitido, que foi o que eles fizeram naquele papel vinculado.

Outra opção é compilar o wasm para algo que você acredita que possa lidar com o fluxo de controle de forma otimizada, ou seja, "desfazer" a estruturação. O LLVM deve ser capaz de fazer isso, portanto, executar wasm em uma VM que usa LLVM (como WAVM ou wasmer) ou por meio de WasmBoxC pode ser interessante. Talvez você possa desabilitar as otimizações do CFG no LLVM e ver o quanto isso importa.

@taralx

Interessante, eu perdi algo sobre tempos de compilação ou uso de memória? O fluxo de controle estruturado deve realmente ser melhor lá - por exemplo, é muito simples ir para o formulário SSA a partir disso, comparado a um CFG geral. Esta foi, de fato, uma das razões pelas quais o wasm optou pelo fluxo de controle estruturado em primeiro lugar. Isso também é medido com muito cuidado porque afeta os tempos de carregamento na Web.

(Ou você quer dizer desempenho do compilador na máquina do desenvolvedor? É verdade que o wasm se inclina na direção de fazer mais trabalho lá e menos no cliente.)

Eu quis dizer o desempenho de compilação no incorporador, mas parece que isso está sendo tratado como um bug , não necessariamente um problema de desempenho puro?

@taralx

Sim, acho que é um bug. Isso só acontece em uma camada em uma VM. E não há razão fundamental para isso - o fluxo de controle estruturado não requer mais recursos, deveria exigir menos. Ou seja, eu aposto que esses bugs de perf seriam mais prováveis ​​de acontecer se wasm tivesse gotos.

@kripken

O fluxo de controle estruturado deve realmente ser melhor lá - por exemplo, é muito simples ir para o formulário SSA a partir disso, comparado a um CFG geral. Esta foi, de fato, uma das razões pelas quais o wasm optou pelo fluxo de controle estruturado em primeiro lugar. Isso também é medido com muito cuidado porque afeta os tempos de carregamento na Web.

Uma pergunta muito específica, apenas no caso: você conhece algum compilador Wasm que realmente faz isso - "muito simples" indo de "fluxo de controle estruturado" para o formulário SSA. Porque de uma olhada rápida, o fluxo de controle do Wasm não é tão (totalmente/em última análise) estruturado. Controle formalmente estruturado é aquele onde não há break s, continue s, return s (grosso modo, o modelo de programação de Scheme, sem mágica como call/cc). Quando estes estão presentes, tal fluxo de controle pode ser chamado de "semi-estruturado".

Existe um algoritmo SSA bem conhecido para fluxo de controle totalmente estruturado: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4503 . Aqui está o que ele tem a dizer sobre o fluxo de controle semiestruturado:

Para instruções estruturadas, mostramos como gerar o formulário SSA e a árvore dominadora em uma única passagem durante a análise. Na seção a seguir, mostraremos que é possível estender nosso método para uma determinada classe de instruções não estruturadas (LOOP/EXIT e RETURN) que podem causar saídas de estruturas de controle em pontos arbitrários. No entanto, como essas saídas são uma espécie de goto (disciplinado), não é de surpreender que sejam muito mais difíceis de lidar do que instruções estruturadas.

OTOH, existe outro algoritmo bem conhecido, https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf que provavelmente também é de passagem única, mas não tem problemas não apenas com controle não estruturado fluxo, mas mesmo com fluxo de controle irredutível (embora não produza resultado ótimo para isso).

Então, a questão é novamente se você sabe que algum projeto passou pelo problema de realmente estender o algoritmo Brandis/Mössenböck e alcançou benefícios tangíveis nessa rota em comparação com Braun et al. algoritmo (como uma nota lateral, meu palpite intuitivo é que Braun algo é exatamente uma extensão "limite superior", embora eu seja muito burro para provar isso intuitivamente para mim mesmo, sem falar sobre uma prova formal, então é isso - palpite intuitivo ).

E o tema geral da questão é estabelecer (embora eu diga "manter") a razão final pela qual Wasm optou por não aceitar o suporte arbitrário de goto. Porque assistindo a este tópico por anos, o modelo mental que construí é que é feito para evitar enfrentar CFGs irredutíveis . E, de fato, o abismo está entre CFGs redutíveis e irredutíveis , com muitos algoritmos de otimização sendo (muito) mais fáceis para os CFGs redutíveis , e é isso que muitos otimizadores codificaram. O fluxo de controle (semi)estruturado no Wasm é apenas uma maneira barata de garantir redutibilidade.

A menção de qualquer facilidade especial de produção de SSA para CFG estruturado (e Wasm CFGs realmente não parece ser estruturado no sentido formal) de alguma forma obscurece a imagem clara acima. Por isso estou perguntando se há referências específicas de que a construção do SSA é praticamente beneficiada pelo formulário Wasm CFG.

Obrigado.

@kripken Estou um pouco confuso agora e ansioso para aprender. Estou analisando a situação e atualmente vejo o seguinte:


A fonte do seu programa tem um certo fluxo de controle. Este CFG é redutível ou não, por exemplo, goto foi usado no idioma de origem ou não. Não há como mudar esse fato. Este CFG pode ser transformado em código de máquina, por exemplo, como o compilador Go faz nativamente.

Se o CFG já for redutível, então está tudo bem e a wasm VM pode carregá-lo rapidamente. Qualquer passagem de tradução deve ser capaz de detectar que este é o caso simples e fazer a coisa mais rápida. Permitir CFGs irredutíveis não deve retardar este caso.

Se o CFG não for redutível, existem duas opções:

  • O compilador o torna redutível, por exemplo, introduzindo uma tabela de salto. Esta etapa perde informações. É difícil restaurar o CFG original sem ter uma análise específica do compilador que produziu o binário. Devido a essa perda de informações, qualquer código de máquina gerado será um pouco mais lento do que o código gerado a partir do CFG inicial. Podemos ser capazes de gerar esse código de máquina com um algoritmo de passagem única, mas é à custa da perda de informações. [1]

  • Permitimos que o compilador emita um CFG irredutível. A VM pode ter que torná-lo redutível. Isso diminui o tempo de carregamento, mas apenas nos casos em que o CFG realmente não é redutível. O compilador tem a opção de escolher entre otimizar o desempenho do tempo de carregamento ou o desempenho do tempo de execução.

[1] Estou ciente de que não é realmente uma perda de informação se ainda houver alguma maneira de reverter a operação, mas não poderia descrevê-la de uma maneira melhor.


Onde está a falha no meu pensamento?

@pfalcon

Você conhece algum compilador Wasm que realmente faz isso - "muito simples" indo de "fluxo de controle estruturado" para o formulário SSA.

Sobre VMs: não sei diretamente. Mas o IIRC na época @lukewagner disseram que era conveniente implementar dessa maneira - talvez um deles possa elaborar. Não tenho certeza se a irredutibilidade era toda a questão ou não. E não tenho certeza se eles implementaram esses algoritmos que você mencionou ou não.

Sobre outras coisas além de VMs: O otimizador Binaryen definitivamente se beneficia do fluxo de controle estruturado do wasm, e não apenas por ser redutível. Várias otimizações são mais simples porque sempre sabemos onde estão os cabeçalhos de loop, por exemplo, que são anotados no wasm. (OTOH outras otimizações são mais difíceis de fazer, e também temos um CFG IR geral para elas...)

@neelance

Se o CFG já for redutível, então está tudo bem e a wasm VM pode carregá-lo rapidamente. Qualquer passagem de tradução deve ser capaz de detectar que este é o caso simples e fazer a coisa mais rápida. Permitir CFGs irredutíveis não deve retardar este caso.

Talvez eu não esteja te entendendo completamente. Mas que uma VM wasm possa carregar código rapidamente depende não apenas de ser redutível ou não, mas de como é codificado. Especificamente, poderíamos ter imaginado um formato que é um CFG geral e, em seguida, a VM precisa fazer o trabalho para verificar se é redutível. Wasm optou por evitar esse trabalho - a codificação é necessariamente redutível (ou seja, ao ler o wasm e fazer a validação trivial, você também está provando que é redutível sem fazer nenhum trabalho extra).

Além disso, a codificação do wasm não dá apenas uma garantia de redutibilidade sem a necessidade de verificar isso. Ele também anota cabeçalhos de loop, ifs e outras coisas úteis (como mencionei separadamente anteriormente neste comentário). Não tenho certeza de quanto as VMs de produção se beneficiam disso, mas espero que sim. (Talvez especialmente em compiladores de linha de base?)

No geral, acho que permitir CFGs irredutíveis pode retardar o caso rápido, a menos que os irredutíveis sejam codificados de maneira separada (como os funclets são propostos).

@kripken

Obrigado pela sua explicação.

Sim, esta é exatamente a diferenciação que estou tentando fazer: vejo a vantagem da notação/codificação estruturada para o caso CFG redutível. Mas não deve ser difícil adicionar algum constructo que permita a notação de um CFG irredutível e ainda manter as vantagens existentes no caso de um CFG de fonte redutível (por exemplo se você não usar este novo constructo, então o CFG é garantido ser redutível).

Como conclusão, não vejo como se pode argumentar que uma notação puramente redutível é mais rápida. No caso de uma fonte redutível CFG é igualmente rápido. E no caso de uma fonte irredutível CFG pode-se, no máximo, argumentar que não é significativamente mais lento, mas alguns casos do mundo real já mostraram que isso é improvável que seja o caso em geral.

Em suma, não vejo como as considerações de desempenho podem ser um argumento que impede o fluxo de controle irredutível e isso me faz questionar por que a próxima etapa precisa ser coletar dados de desempenho.

@neelance

Sim, concordo que poderíamos adicionar uma nova construção - como funclets - e, ao não usá-la, não diminuiria a velocidade do caso existente.

Mas há uma desvantagem em adicionar qualquer nova construção, pois adiciona complexidade ao wasm. Em particular, significa uma área de superfície maior nas VMs, o que significa mais possíveis bugs e problemas de segurança. Wasm se inclinou para ter o máximo de complexidade do lado do desenvolvedor sempre que possível para reduzir a complexidade na VM.

Algumas propostas de wasm não são apenas sobre velocidade, como GC (que permite a coleta de ciclos com JS). Mas para propostas que tratam de velocidade, como funclets, precisamos mostrar que a velocidade justifica a complexidade. Tivemos esse debate sobre o SIMD, que também é sobre velocidade, e decidimos que valia a pena porque vimos que ele pode atingir velocidades muito grandes de forma confiável no código do mundo real (2x ou até mais).

(Há outros benefícios além da velocidade para permitir CFGs gerais, eu concordo, como tornar mais fácil para os compiladores direcionarem o wasm. Mas podemos resolver isso sem adicionar complexidade às VMs wasm. Já fornecemos suporte para CFGs arbitrários em LLVM e Binaryen , permitindo que os compiladores emitam CFGs e não se preocupem com o fluxo de controle estruturado. Se isso não for bom o suficiente, nós - pessoas de ferramentas quero dizer, inclusive eu - deveríamos fazer mais.)

Funclets não são sobre velocidade, mas sim sobre permitir que linguagens com fluxo de controle não trivial sejam compiladas para WebAssembly, C e Go sendo as mais óbvias, mas se aplica a qualquer linguagem que tenha async/await. Além disso, a escolha de ter fluxo de controle hierárquico na verdade leva a _mais_ bugs em VMs, como evidenciado pelo fato de que todos os compiladores Wasm, exceto V8, decompõem o fluxo de controle hierárquico em um CFG de qualquer maneira. Os EBBs em um CFG podem representar as várias construções de fluxo de controle no Wasm e muito mais, e ter uma única construção para compilar leva a muito menos bugs do que ter muitos tipos diferentes com diferentes usos.

Até o Lightbeam, um compilador de streaming muito simples, viu uma enorme diminuição nos erros de compilação depois de adicionar uma etapa extra de tradução que decompôs o fluxo de controle em um CFG. Isso vale o dobro para o outro lado desse processo - o Relooper é muito mais propenso a erros do que a emissão de funclets, e os desenvolvedores que trabalham nos backends Wasm para LLVM e outros compiladores que eram funclets a serem implementados emitiriam todos os função usando apenas funclets, a fim de melhorar a confiabilidade e simplicidade do codegen. Todos os compiladores que produzem Wasm usam EBBs, todos menos um dos compiladores que consomem Wasm usam EBBs, essa recusa em implementar funclets ou alguma outra maneira de representar CFGs é simplesmente adicionar uma etapa com perdas que prejudica todas as partes envolvidas, exceto a equipe V8 .

"Fluxo de controle irredutível considerado prejudicial" é apenas um ponto de discussão, você pode facilmente adicionar a restrição de que o fluxo de controle dos funclets seja redutível e, se você quiser permitir o fluxo de controle irredutível no futuro, todos os módulos Wasm existentes com fluxo de controle redutível funcionariam sem modificações em um motor que suporta adicionalmente fluxo de controle irredutível. Seria simplesmente o caso de remover a verificação de redutibilidade no validador.

@Vurich

Você pode adicionar facilmente a restrição de que o fluxo de controle dos funclets seja redutível

Você pode, mas não é trivial - as VMs precisariam verificar isso. Não acho que isso seja possível em uma única passagem linear, o que seria um problema para compiladores de linha de base, que agora estão presentes na maioria das VMs. (Na verdade, apenas encontrar backedges de loop - que é um problema mais simples e necessário por outros motivos também - não pode ser feito em uma única passagem para frente, pode?)

todos os compiladores Wasm diferentes do V8 decompõem o fluxo de controle hierárquico para um CFG de qualquer maneira.

Você está se referindo à abordagem "mar de nós" que o TurboFan usa? Eu não sou um especialista nisso, então vou deixar para os outros responderem.

Mas, de forma mais geral, mesmo que você não compre o argumento acima para otimizar compiladores, é ainda mais diretamente verdadeiro para compiladores de linha de base, conforme mencionado anteriormente.

Funclets não são sobre velocidade tanto quanto sobre permitir que linguagens com fluxo de controle não trivial compilem para WebAssembly [..] Relooper é muito mais propenso a erros do que emitir funclets

Concordo 100% no lado das ferramentas. É mais difícil emitir código estruturado da maioria dos compiladores! Mas o ponto é que isso torna mais simples no lado da VM, e foi isso que a wasm escolheu fazer. Mas, novamente, concordo que isso tem desvantagens, incluindo as desvantagens que você mencionou.

Será que estava errado em 2015? É possível. Eu acho que nós mesmos erramos em algumas coisas (como depuração e a mudança tardia para uma máquina de pilha). Mas não é possível corrigi-los em retrospecto, e há um alto nível para adicionar coisas novas, especialmente as que se sobrepõem.

Dado tudo isso, tentando ser construtivo, acho que devemos corrigir os problemas existentes no lado das ferramentas. Há uma barra muito, muito mais baixa para mudanças de ferramentas. Duas sugestões possíveis:

  • Eu posso olhar para portar o código Binaryen CFG para Go, se isso ajudar o compilador Go - @neelance ?
  • Podemos implementar funclets ou algo parecido puramente no lado das ferramentas. Ou seja, fornecemos código de biblioteca para isso hoje, mas também podemos adicionar um formato binário. (Já existe precedente para adicionar ao formato binário wasm no lado das ferramentas, em arquivos de objeto wasm.)

Podemos implementar funclets ou algo parecido puramente no lado das ferramentas. Ou seja, fornecemos código de biblioteca para isso hoje, mas também podemos adicionar um formato binário. (Já existe precedente para adicionar ao formato binário wasm no lado das ferramentas, em arquivos de objeto wasm.)

Se houver algum trabalho concreto feito sobre isso, vale a pena notar que (AFAIU) a menor maneira idiomática de adicionar isso ao Wasm (como @rossberg aludiu ) seria introduzir a instrução de bloco

multiloop (t in ) _n_ t out (_instr_* end ) _n_

que define n corpos rotulados (com n anotações de tipo de entrada declaradas para frente). A família de instruções br é então generalizada para que todos os rótulos definidos pelo multiloop estejam dentro do escopo de cada corpo, em ordem (como em, qualquer corpo pode ser ramificado de dentro de qualquer outro corpo). Quando um corpo multiloop é ramificado, a execução salta para o _start_ do corpo (como um loop Wasm regular). Quando a execução atinge o final de um corpo sem ramificar para outro corpo, toda a construção retorna (sem fall-through).

Haveria algum problema a ser feito sobre como representar eficientemente as anotações de tipo de cada corpo (na formulação acima, n corpos podem ter n tipos de entrada diferentes, mas todos devem ter o mesmo tipo de saída, então não posso usar diretamente índices _blocktype_ regulares de vários valores sem exigir um cálculo LUB de sentimento supérfluo) e como selecionar o corpo inicial a ser executado (sempre o primeiro, ou deve haver um parâmetro estático?).

Isso obtém o mesmo nível de expressividade dos funclets, mas evita a introdução de um novo espaço de instruções de controle. Na verdade, se os funclets tivessem sido iterados ainda mais, acho que teria se transformado em algo assim.

EDIT: ajustar isso para ter um comportamento de queda complicaria marginalmente a semântica formal, mas provavelmente seria melhor para o caso de uso de @neelance e poderia ajudar a sugerir a um compilador de linha de base qual é o caminho do fluxo de controle no rastreamento.

O princípio do projeto Wasm de descarregar o trabalho nas ferramentas para tornar os motores mais simples/rápidos é muito importante e continuará sendo muito benéfico.

Dito isto, como tudo que não é trivial, é uma troca, não preto e branco. Acredito que aqui temos um caso em que a dor dos produtores é desproporcional à dor dos motores. A maioria dos compiladores que gostaríamos de trazer para o Wasm usa estruturas CFG arbitrárias internamente (SSA) ou são usadas para direcionar coisas que não se importam com gotos (CPUs). Estamos fazendo o mundo pular por aros sem muito ganho.

Algo como funclets (ou multiloop) é bom porque é modular: se um produtor não precisar, as coisas funcionarão como antes. Se um motor realmente não pode lidar com CFGs arbitrários, então, no momento, eles podem emiti-lo como se fosse um tipo de construção loop + br_table , e apenas aqueles que o usam pagam o preço . Então, "o mercado decide" e vemos se há pressão nos motores para emitir um código melhor para isso. Algo me diz que se houver muito código Wasm que depende de funclets, não será um desastre tão grande para os mecanismos emitirem código bom para eles como algumas pessoas parecem pensar.

Você pode, mas não é trivial - as VMs precisariam verificar isso. Não acho que isso seja possível em uma única passagem linear, o que seria um problema para compiladores de linha de base, que agora estão presentes na maioria das VMs.

Talvez eu esteja entendendo mal as expectativas de um compilador de linha de base, mas por que eles se importariam? Se você vir um goto, insira uma instrução de salto.

Concordo 100% no lado das ferramentas. É mais difícil emitir código estruturado da maioria dos compiladores! Mas o ponto é que isso torna mais simples no lado da VM, e foi isso que a wasm escolheu fazer. Mas, novamente, concordo que isso tem desvantagens, incluindo as desvantagens que você mencionou.

Não, como eu disse várias vezes no meu comentário original, _não_ facilita as coisas no lado da VM. Trabalhei em um compilador de linha de base por mais de um ano e minha vida ficou mais fácil e o código emitido ficou mais rápido depois que adicionei uma etapa provisória que converteu o fluxo de controle do Wasm em um CFG.

Você pode, mas não é trivial - as VMs precisariam verificar isso. Não acho que isso seja possível em uma única passagem linear, o que seria um problema para compiladores de linha de base, que agora estão presentes na maioria das VMs. (Na verdade, apenas encontrar backedges de loop - que é um problema mais simples e necessário por outros motivos também - não pode ser feito em uma única passagem para frente, pode?)

Ok, é o seguinte, meu conhecimento dos algoritmos usados ​​em compiladores não é forte o suficiente para afirmar com absoluta certeza que o fluxo de controle irredutível pode ou não ser detectado em um compilador de streaming, mas o fato é que não precisa ser. A verificação pode acontecer em conjunto com a compilação. Se um algoritmo de streaming não existir, o que nem você nem eu sabemos que não existe, você pode usar um algoritmo sem streaming assim que a função for totalmente recebida. Se (por algum motivo) o fluxo de controle irredutível levar a algo realmente ruim como um loop infinito, você pode simplesmente expirar o tempo limite da compilação e/ou cancelar o thread de compilação. No entanto, não há razão para acreditar que este seria o caso.

Talvez eu esteja entendendo mal as expectativas de um compilador de linha de base, mas por que eles se importariam? Se você vir um goto, insira uma instrução de salto.

Não é tão simples por causa de como você precisa mapear a máquina de registradores infinitos do Wasm (não, não é uma máquina de pilha ) para os registradores finitos do hardware físico, mas isso é um problema que qualquer compilador de streaming deve resolver e é totalmente ortogonal para CFGs vs fluxo de controle hierárquico.

O compilador de streaming em que trabalhei pode compilar um CFG arbitrário - até irredutível - muito bem. Não está fazendo nada particularmente especial. Você simplesmente atribui a cada bloco uma "convenção de chamada" (basicamente o local onde os valores no escopo desse bloco devem estar) quando precisar pular para ele pela primeira vez e se chegar a um ponto em que precisa ramificar condicionalmente para dois ou mais destinos com "convenções de chamada" incompatíveis, você envia um bloco "adaptador" para uma fila e o emite no próximo ponto possível. Isso pode acontecer tanto com fluxo de controle redutível quanto irredutível, e quase nunca é necessário em ambos os casos. O argumento do "fluxo de controle irredutível considerado prejudicial", como eu disse antes, é um ponto de discussão e não um argumento técnico. Representar o fluxo de controle como um CFG torna os compiladores de streaming muito mais fáceis de escrever e, como já disse várias vezes, sei disso por extensa experiência pessoal.

Quaisquer casos em que o fluxo de controle irredutível torna as implementações mais difíceis de escrever, das quais não consigo pensar em nenhuma, podem apenas ser apagadas e retornar um erro, e se você precisar de um algoritmo separado, sem streaming, para detectar com 100% de certeza esse controle o fluxo é irredutível (para que você não aceite acidentalmente o fluxo de controle irredutível), então isso pode ser executado separadamente do próprio compilador de linha de base. Alguém que eu tenho motivos para acreditar que é uma autoridade no assunto (embora eu evite invocá-los porque sei que eles não querem ser arrastados para este tópico) existe um algoritmo de streaming relativamente simples para detectar a irredutibilidade de um CFG, mas não posso dizer em primeira mão que isso seja verdade.

@oridb

Talvez eu esteja entendendo mal as expectativas de um compilador de linha de base, mas por que eles se importariam? Se você vir um goto, insira uma instrução de salto.

Compiladores de linha de base ainda precisam fazer coisas como inserir verificações extras em backedges de loop (é assim que na Web uma página suspensa mostrará um diálogo de script lento eventualmente), então eles precisam identificar coisas assim. Além disso, eles tentam fazer alocação de registro razoavelmente eficiente (compiladores de linha de base geralmente executam cerca de 1/2 da velocidade do compilador de otimização - o que é muito impressionante, pois são de passagem única!). Ter a estrutura do fluxo de controle, incluindo junções e divisões, torna isso muito mais fácil.

@gwvo

Dito isto, como tudo que não é trivial, é uma troca, não preto e branco. [..] Estamos fazendo o mundo pular por aros sem muito ganho.

Concordo totalmente que é uma troca, e até talvez eu tenha entendido errado naquela época. Mas acredito que seja muito mais prático consertar esses aros do lado das ferramentas.

Então, "o mercado decide" e vemos se há pressão nos motores para emitir um código melhor para isso.

Na verdade, isso é algo que evitamos até agora. Tentamos tornar o wasm o mais simples possível na VM para que não exija otimizações complexas - nem mesmo coisas como inlining, tanto quanto possível. O objetivo é fazer o trabalho duro no lado das ferramentas, não pressionar as VMs a fazer melhor.

@Vurich

Trabalhei em um compilador de linha de base por mais de um ano e minha vida ficou mais fácil e o código emitido ficou mais rápido depois que adicionei uma etapa provisória que converteu o fluxo de controle do Wasm em um CFG.

Muito interessante! Qual VM foi essa?

Eu também ficaria especificamente curioso se fosse um single-pass/streaming ou não (se fosse, como ele lidava com a instrumentação de backedge de loop?), e como ele registra a alocação.

Em princípio, tanto backedges de loop quanto alocação de registradores podem ser tratados com base na ordem linear de instruções, na expectativa de que os blocos básicos sejam colocados em alguma ordem razoável do tipo topsort, sem exigir estritamente isso.

Para backedges de loop: Defina um backedge como uma instrução que salta para o início do fluxo de instruções. Na pior das hipóteses, se os blocos forem dispostos ao contrário, você obterá mais verificações de backedge do que o estritamente necessário.

Para alocação de registro: Esta é apenas a alocação de registro de varredura linear padrão. O tempo de vida de uma variável para alocação de registro abrange desde a primeira menção da variável até a última menção, incluindo todos os blocos linearmente intermediários. Na pior das hipóteses, se os blocos forem embaralhados, você terá uma vida útil mais longa do que o necessário e, portanto, derramará coisas desnecessariamente na pilha. O único custo extra é rastrear a primeira e a última menção de cada variável, o que pode ser feito para todas as variáveis ​​com uma única varredura linear. (Para wasm, suponho que uma "variável" seja um slot local ou de pilha.)

@kripken

Eu posso olhar para portar o código Binaryen CFG para Go, se isso ajudar o compilador Go - @neelance ?

Para integrar o Asyncify? Comente a proposta .

@comex

Bons pontos!

O único custo extra é rastrear a primeira e a última menção de cada variável

Sim, acho que é uma diferença significativa. A alocação de registro de varredura linear é melhor (mas mais lenta de fazer) do que os compiladores de linha de base wasm atualmente , pois compilam em uma maneira de streaming que é muito rápida. Ou seja, não há um passo inicial para encontrar a última menção de cada variável - eles compilam em uma única passagem, emitindo código à medida que avançam sem sequer ver código posteriormente na função wasm, auxiliados pela estrutura, e também simplificam escolhas à medida que vão ("estúpido" é a palavra usada nesse post).

A abordagem de streaming do V8 para alocação de registros deve funcionar tão bem se os blocos puderem ser mutuamente recursivos (como em https://github.com/WebAssembly/design/issues/796#issuecomment-742690194), já que os únicos tempos de vida com os quais lidam são vinculados dentro de um único bloco (pilha) ou assumidos para toda a função (local).

IIUC (com referência ao comentário de @titzer ) o principal problema para o V8 está no tipo de CFGs que o Turbofan pode otimizar.

@kripken

Tentamos tornar o wasm o mais simples possível na VM para que não exija otimizações complexas

Esta não é uma "otimização complexa".. gotos são incrivelmente básicos e naturais para muitos sistemas. Aposto que há muitos motores que poderiam adicionar isso sem nenhum custo. Tudo o que estou dizendo é que se há motores que querem manter um modelo CFG estruturado por qualquer motivo, eles podem.

Por exemplo, tenho certeza de que o LLVM (de longe nosso produtor número 1 de Wasm atualmente) não mudará para o uso de funclets até que esteja confiante de que não é uma regressão de desempenho nos principais mecanismos.

@kripken Faz parte do Wasmtime. Sim, é streaming e foi planejado para ser de complexidade O(N), mas eu mudei para uma nova empresa antes que isso fosse totalmente realizado, então é apenas "O(N)-ish". https://github.com/bytecodealliance/wasmtime/tree/main/crates/lightbeam

Obrigado @Vurich , interessante. Seria ótimo ver os números de desempenho quando estiverem disponíveis, especialmente para inicialização, mas também para a taxa de transferência. Eu diria que sua abordagem compilaria mais lentamente do que a abordagem adotada pelos engenheiros do V8 e do SpiderMonkey, ao mesmo tempo em que emite código mais rápido. Portanto, é uma troca diferente neste espaço. Parece plausível que sua abordagem não se beneficie do fluxo de controle estruturado do wasm, como você disse, enquanto a deles o faz.

Não, é um compilador de streaming e emite código mais rápido do que qualquer um desses dois mecanismos (embora existam casos degenerados que não foram corrigidos no momento em que saí do projeto). Embora eu tenha feito o meu melhor para emitir código rápido, ele foi projetado principalmente para emitir código rapidamente, com a eficiência da saída sendo uma preocupação secundária. O custo de inicialização é, que eu saiba, zero (acima do custo inerente do Wasmtime que é compartilhado entre back-ends) porque toda estrutura de dados começa não inicializada e a compilação é feita instrução por instrução. Embora eu não tenha números para comparar com o V8 ou o SpiderMonkey, tenho números para comparar com o Cranelift (o mecanismo principal na época). Eles estão vários meses desatualizados neste momento, mas você pode ver que não apenas emite código mais rápido que o Cranelift, mas também emite código mais rápido que o Cranelift. Na época, ele também emitia código mais rápido que o SpiderMonkey, embora você tenha que acreditar na minha palavra, então não vou culpá-lo se você não acreditar em mim. Embora eu não tenha números mais recentes à mão, acredito que o estado agora é que tanto o Cranelift quanto o SpiderMonkey corrigiram o pequeno punhado de bugs que eram a principal fonte de sua saída de baixo desempenho nesses microbenchmarks quando comparados ao Lightbeam, mas o diferencial de velocidade de compilação não mudou durante todo o tempo em que estive no projeto porque cada compilador ainda é fundamentalmente arquitetado da mesma forma, e é a respectiva arquitetura que leva aos diferentes níveis de desempenho. Embora eu aprecie sua especulação, não sei de onde vem sua suposição de que o método que descrevi seria mais lento.

Aqui estão os benchmarks, os benchmarks ::compile são para velocidade de compilação e os benchmarks ::run são para velocidade de execução da saída do código de máquina. https://gist.github.com/Vurich/8696e67180aa3c93b4548fb1f298c29e

A metodologia está aqui, você pode cloná-lo e executar novamente os benchmarks para confirmar os resultados por si mesmo, mas o PR provavelmente será incompatível com a versão mais recente do wasmtime, então ele só mostrará a comparação de desempenho na última vez que atualizei o PR. https://github.com/bytecodealliance/wasmtime/pull/1660

Dito isto, meu argumento é _não_ que CFGs são uma representação interna útil para desempenho em um compilador de streaming. Meu argumento é que os CFGs não afetam negativamente o desempenho em nenhum compilador, e certamente não no nível que justificaria bloquear totalmente as equipes GCC e Go de produzir WebAssembly. Quase ninguém neste tópico argumentando contra funclets ou uma extensão semelhante ao wasm realmente trabalhou nos projetos que eles alegam que serão afetados negativamente por esta proposta. Para não dizer que você precisa de experiência em primeira mão para comentar sobre este tópico, acho que todo mundo tem algum nível de contribuição valiosa, mas é para dizer que há uma linha entre ter uma opinião diferente sobre a cor do bicicletário e fazer alegações baseadas em nada mais do que especulações ociosas.

@Vurich

Não, é um compilador de streaming e emite código mais rápido que qualquer um desses dois mecanismos (embora existam casos degenerados que nunca foram corrigidos porque saí do projeto).

Desculpe se não fui claro o suficiente antes. Para ter certeza de que estamos falando da mesma coisa, eu quis dizer os compiladores básicos desses mecanismos. E estou falando sobre o tempo de compilação, que é o ponto dos compiladores de linha de base no sentido de que V8 e SpiderMonkey usam o termo.

A razão que eu sou cético você pode bater V8 e SpiderMonkey vezes o valor basal de compilação é porque, como nas ligações que eu dei anteriormente, esses dois compiladores de linha de base são extraordinariamente atento para tempo de compilação. Em particular, eles não geram nenhum IR interno, apenas vão direto do wasm para o código de máquina. Você disse que seu compilador emite um IR interno (para um CFG) - eu esperaria que seus tempos de compilação fossem mais lentos apenas por causa disso (devido a mais ramificações, largura de banda de memória etc.).

Mas, por favor, compare com esses compiladores de linha de base! Eu adoraria ver dados mostrando que meu palpite está errado, e tenho certeza que os engenheiros do V8 e do SpiderMonkey também. Isso significaria que você encontrou um design melhor que eles deveriam considerar adotar.

Para testar contra o V8, você pode executar d8 --liftoff --no-wasm-tier-up , e para SpiderMonkey você pode executar sm --wasm-compiler=baseline .

(Obrigado pelas instruções para comparar com o Cranelift, mas o Cranelift não é um compilador de linha de base, portanto, comparar os tempos de compilação com ele não é relevante neste contexto. De outra forma, muito interessante, concordo.)

Minha intuição é que os compiladores de linha de base não teriam que alterar significativamente sua estratégia de compilação para suportar funclets/ multiloop , já que eles não tentam fazer uma otimização significativa entre blocos de qualquer maneira. A "estrutura de fluxo de controle confiável, incluindo junções e divisões" referenciada por @kripken é satisfeita por exigir que todos os tipos de entrada para uma coleção de blocos mutuamente recursivos sejam declarados para frente (o que parece ser a escolha natural para validação de streaming de qualquer maneira) . Se o Lightbeam/Wasmtime pode superar os compiladores de linha de base do mecanismo, isso não leva em conta; o ponto importante é se os compiladores de linha de base do mecanismo podem permanecer tão rápidos quanto são agora.

FWIW, eu estaria interessado em ver esse recurso trazido para discussão em uma futura reunião de CG e concordo amplamente com @Vurich que os representantes do mecanismo podem se opor se não estiverem preparados para implementá-lo. Dito isto, devemos levar a sério qualquer objeção desse tipo (eu já opinei em reuniões pessoais que, ao buscar esse recurso, devemos tentar evitar uma versão WebAssembly da saga JavaScript

@kripken

Sim, acho que é uma diferença significativa. A alocação de registro de varredura linear é melhor (mas mais lenta de fazer) do que os compiladores de linha de base wasm atualmente , pois compilam em uma maneira de streaming que é muito rápida. Ou seja, não há um passo inicial para encontrar a última menção de cada variável - eles compilam em uma única passagem, emitindo código à medida que avançam sem sequer ver código posteriormente na função wasm, auxiliados pela estrutura, e também simplificam escolhas à medida que vão ("estúpido" é a palavra usada nesse post).

Uau, isso realmente é muito simples.

Por outro lado... esse algoritmo em particular é tão simples que não depende de nenhuma propriedade profunda do fluxo de controle estruturado. Quase não depende das propriedades superficiais do fluxo de controle estruturado.

Como a postagem do blog menciona, o compilador de linha de base wasm do SpiderMonkey não preserva o estado do alocador de registro por meio de "junções de fluxo de controle" (ou seja, blocos básicos com vários predecessores), em vez disso, usando uma ABI fixa ou mapeando da pilha wasm para a pilha nativa e registros . Descobri através de testes que ele também usa uma ABI fixa ao inserir blocks , mesmo que isso não seja uma junção de fluxo de controle na maioria dos casos!

A ABI fixa é a seguinte (em x86):

  • Se houver um número diferente de zero de parâmetros (ao entrar em um bloco) ou retornos (ao sair de um bloco), o topo da pilha wasm vai em rax e o restante da pilha wasm corresponde ao x86 pilha.
  • Caso contrário, toda a pilha wasm corresponde à pilha x86.

Por que isso importa?

Porque esse algoritmo poderia funcionar quase da mesma maneira com muito menos informações. Como um experimento de pensamento, imagine uma versão de universo alternativo do WebAssembly onde não havia instruções de fluxo de controle estruturadas, apenas instruções de salto, semelhantes ao assembly nativo. Teria que ser aumentado com apenas uma informação extra: uma maneira de dizer quais instruções são os alvos dos saltos.

Então o algoritmo seria apenas: percorrer as instruções de forma linear; antes de saltos e alvos de salto, nivele os registros para a ABI fixa.

A única diferença é que teria que haver um único ABI fixo, não dois. Ele não conseguia distinguir entre o valor do topo da pilha sendo semanticamente o 'resultado' de um salto, versus apenas ser deixado na pilha de um bloco externo. Portanto, teria que colocar incondicionalmente o topo da pilha em rax .

Mas duvido que isso tenha algum custo mensurável para o desempenho; se alguma coisa, pode ser uma melhoria.

(A verificação também seria diferente, mas ainda de passagem única.)

Ok, ressalvas iniciais:

  1. Este não é um universo alternativo; estamos presos a fazer extensões compatíveis com versões anteriores para o WebAssembly existente.
  2. O compilador de linha de base do SpiderMonkey é apenas uma implementação, e é possível que seja abaixo do ideal em relação à alocação de registros: se fosse um pouco mais inteligente, o benefício do tempo de execução superaria o custo do tempo de compilação.
  3. Mesmo que os compiladores de linha de base não precisem de informações adicionais, os compiladores de otimização podem precisar delas para uma construção rápida de SSA.

Com isso em mente, o experimento de pensamento acima fortalece minha crença de que compiladores de linha de base não precisam de fluxo de controle estruturado . Independentemente de quão baixo nível uma construção adicionamos, desde que inclua informações básicas como quais instruções são alvos de salto, os compiladores de linha de base podem lidar com isso apenas com pequenas alterações. Ou pelo menos este pode.

@conrad-watt @comex

São pontos muito bons! Minha intuição sobre compiladores de linha de base pode estar errada então.

E @comex - sim, como você disse, essa discussão é separada da otimização de compiladores onde o SSA pode se beneficiar da estrutura. Talvez valha a pena citar um pouco de um dos links anteriores :

Por design, transformar o código do WebAssembly no IR do TurboFan (incluindo a construção SSA) em uma única passagem direta é muito eficiente, parcialmente devido ao fluxo de controle estruturado do WebAssembly.

@conrad-watt Eu definitivamente concordo que só precisamos obter feedback direto do pessoal da VM e manter a mente aberta. Para ser claro, meu objetivo aqui não é impedir nada. Comentei aqui extensamente porque vários comentários pareciam pensar que o fluxo de controle estruturado do wasm era um erro óbvio ou que obviamente deveria ser remediado com funclets/multiloop - eu só queria apresentar a história do pensamento aqui e que havia fortes razões para o modelo atual, por isso pode não ser fácil de melhorar.

Gostei muito de ler esta conversa. Eu mesmo me perguntei um monte dessas perguntas (vindos de ambas as direções) e compartilhei muitos desses pensamentos (novamente de ambas as direções), e a discussão ofereceu muitos insights e experiências úteis. Não tenho certeza se tenho uma opinião forte ainda, mas tenho um pensamento de contribuir em cada direção.

No lado "for", é útil saber antecipadamente quais blocos têm backedges. Um compilador de streaming pode rastrear propriedades que não são aparentes no sistema de tipos do WebAssembly (por exemplo, o índice em i está dentro dos limites da matriz em arr ). Ao pular para frente, pode ser útil anotar o alvo com quais propriedades são mantidas naquele ponto. Dessa forma, quando um rótulo é alcançado, seu bloco pode ser compilado usando as propriedades que são mantidas em todas as bordas, por exemplo, para eliminar verificações de limites de matriz. Mas se um rótulo pode potencialmente ter um backedge desconhecido, seu bloco não pode ser compilado com esse conhecimento. É claro que um compilador sem streaming pode fazer uma análise invariante de loop mais significativa, mas para um compilador de streaming é útil não ter que se preocupar com o que pode estar por vir. (Pensamento lateral: @Vurich menciona que o WebAssembly não é uma máquina de pilha devido ao uso de locais. Em #1381, expus algumas razões para depender menos de locais e adicionar mais operações de pilha. Tornar a alocação de registradores mais fácil parece ser outra razão em essa direção.)

Do lado "contra", até agora a discussão se concentrou apenas no controle local. Isso é bom para C, mas e para C++ ou várias outras linguagens com exceções semelhantes? E as línguas com outras formas de controle não local? Coisas com escopo dinâmico geralmente são estruturadas de forma inerente (ou pelo menos não conheço nenhum exemplo de escopos dinâmicos mutuamente recursivos). Acho que essas considerações são endereçáveis, mas você teria que projetar algo com elas em mente para que o resultado fosse utilizável nessas configurações. Isso é algo que tenho ponderado e fico feliz em compartilhar meus pensamentos em andamento (parecendo mais ou menos uma extensão do multi-loop de @conrad-watt) com quem estiver interessado (embora aqui pareça fora do tópico), mas Eu queria pelo menos avisar que há mais do que apenas fluxo de controle local a ser lembrado.

(Também gostaria de adicionar mais um +1 para ouvir mais pessoas da VM, embora ache que @kripken está fazendo um ótimo trabalho representando as considerações.)

Quando digo que o Lightbeam produz um IR interno, isso é realmente bastante enganoso e eu deveria ter esclarecido. Eu estava trabalhando no projeto por um tempo e às vezes você pode ter uma visão de túnel. Basicamente, o Lightbeam consome a instrução de entrada por instrução (na verdade, ele tem no máximo uma instrução à frente, mas isso não é particularmente importante), e para cada instrução ele produz, preguiçosamente e em espaço constante, várias instruções IR internas. O número máximo de instruções por instrução Wasm é constante e pequeno, algo como 6. Não está criando um buffer de instruções IR para toda a função e trabalhando nisso. Em seguida, ele lê essas instruções IR uma a uma. Você pode realmente pensar nisso como tendo uma biblioteca de funções auxiliares mais genéricas que implementa cada instrução Wasm em termos de, eu apenas me refiro a ele como um IR porque isso ajuda a explicar como ele tem um modelo diferente para fluxo de controle etc. Ele provavelmente não produz código tão rápido quanto o V8 ou os compiladores de linha de base do SpiderMonkey, mas isso ocorre porque não é totalmente otimizado e não porque é arquiteturalmente deficiente. Meu ponto é que eu modelo internamente o fluxo de controle hierárquico do Wasm como se fosse um CFG, em vez de realmente produzir um buffer de IR na memória da maneira que LLVM ou Cranelift fazem.

Outra opção é compilar o wasm para algo que você acredita que possa lidar com o fluxo de controle de forma otimizada, ou seja, "desfazer" a estruturação. O LLVM deve ser capaz de fazer isso, portanto, executar wasm em uma VM que usa LLVM (como WAVM ou wasmer) ou por meio de WasmBoxC pode ser interessante.

@kripken Infelizmente, o LLVM não parece ser capaz de desfazer a estruturação ainda. O passo de otimização de segmentação de salto deve ser capaz de fazer isso, mas ainda não reconhece esse padrão. Aqui está um exemplo mostrando algum código C++ que imita como o algoritmo do relooper converteria um CFG em um loop+switch. GCC consegue "dereloop", mas clang não: https://godbolt.org/z/GGM9rP

@AndrewScheidecker Interessante, obrigado. Sim, essas coisas podem ser bastante imprevisíveis, portanto, pode não haver opção melhor do que investigar o código emitido (como o artigo "Não tão rápido" vinculado anteriormente) e evitar tentativas de atalhos, como confiar no otimizador do LLVM.

@comex

O compilador de linha de base do SpiderMonkey é apenas uma implementação, e é possível que seja abaixo do ideal em relação à alocação de registros: se fosse um pouco mais inteligente, o benefício do tempo de execução superaria o custo do tempo de compilação.

Poderia ser claramente mais inteligente sobre a alocação de registros. Ele se espalha indiscriminadamente em bifurcações de fluxo de controle, junções e antes de chamadas, e poderia manter mais informações sobre o estado do registrador e tentar manter os valores nos registradores por mais tempo / até que estejam mortos. Ele poderia escolher um registrador melhor que rax para resultados de valores de blocos, ou melhor, não usar um registrador fixo. Ele poderia dedicar estaticamente alguns registradores para armazenar variáveis ​​locais; uma análise de corpus que fiz sugeriu que apenas alguns registros inteiros e FP seriam suficientes para a maioria das funções. Poderia ser mais inteligente sobre derramamento em geral; do jeito que está, ele despeja tudo em pânico quando fica sem registros.

O custo de tempo de compilação disso é principalmente que cada borda do fluxo de controle terá uma quantidade não constante de informações associadas a ela (o estado do registrador) e isso pode levar a um uso mais difundido da alocação dinâmica de armazenamento, que o compilador de linha de base tem. longe evitado. E é claro que haverá um custo associado ao processamento dessas informações de tamanho variável em cada junção (e em outros lugares). Mas já existe algum custo não constante, já que o estado do registrador precisa ser percorrido para gerar o código de derramamento e, em geral, pode haver poucos valores ativos, portanto, isso pode ser OK (ou não). Claro, ser mais esperto com o regalloc pode ou não valer a pena em chips modernos, com seus caches rápidos e execução ooo...

Um custo mais sutil é a manutenção do compilador... ele já é bastante complexo e, como é de passagem única e não constrói um gráfico IR nem usa memória dinâmica, é resistente a camadas e abstração.

@RossTate

Re funclets / gotos, dei uma olhada na especificação do funclet outro dia e, à primeira vista, não parecia que um compilador de uma passagem deveria ter algum problema real com ele, certamente não com um esquema regalloc simplista. Mas mesmo com um esquema melhor, pode ser bom: a primeira aresta a alcançar um ponto de junção decidiria qual é a atribuição do registrador, e outras arestas teriam que se conformar.

@conrad-watt como você acabou de mencionar na reunião de CG, acho que estaríamos muito interessados ​​em ver detalhes sobre como seria seu multi-loop.

@aarappel sim, a vida chegou rápido, mas devo fazer isso na próxima reunião. Apenas para enfatizar que a ideia não é minha desde que @rossberg originalmente a esboçou em resposta ao primeiro rascunho de funclets.

Uma referência que pode ser instrutiva é meio datada, mas generaliza noções familiares de loops para lidar com irredutíveis usando gráficos de DJ .

Tivemos algumas sessões de discussão sobre isso no CG, e escrevi um resumo e um documento de acompanhamento. Por causa do comprimento eu fiz uma essência separada.

https://gist.github.com/conrad-watt/6a620cb8b7d8f0191296e3eb24dffdef

Acho que as duas perguntas imediatas (consulte a seção de acompanhamento para obter mais detalhes) são:

  • Podemos encontrar programas "selvagens" que estão sofrendo atualmente e se beneficiariam de multiloop em termos de desempenho? Estes podem ser programas para os quais as transformações LLVM introduzem um fluxo de controle irredutível, mesmo que não exista no programa de origem.
  • Existe um mundo onde multiloop é implementado primeiro no lado do produtor, com alguma camada de implantação de link/tradução para "Web" Wasm?

Provavelmente também há uma discussão mais livre a ser feita sobre as consequências dos problemas de tratamento de exceções que discuto no documento de acompanhamento e, é claro, sobre detalhes semânticos se avançarmos com algo concreto.

Como essas discussões podem se ramificar um pouco, pode ser apropriado transformar algumas delas em problemas no repositório de funclets .

Estou muito feliz em ver o progresso nesta questão. Um enorme "Obrigado" a todos os envolvidos!

Podemos encontrar programas "selvagens" que estão sofrendo atualmente e se beneficiariam com o desempenho do multiloop? Estes podem ser programas para os quais as transformações LLVM introduzem um fluxo de controle irredutível, mesmo que não exista no programa de origem.

Eu gostaria de alertar um pouco contra o raciocínio circular: programas que atualmente têm desempenho ruim são menos propensos a ocorrer "na natureza" exatamente por esse motivo.

Acho que a maioria dos programas Go deve se beneficiar muito. O compilador Go precisa de corrotinas WebAssembly ou multiloop para poder emitir código eficiente que suporte goroutines Go.

Os casadores de expressão regular pré-compilados, juntamente com outras máquinas de estado pré-compiladas, geralmente resultam em fluxo de controle irredutível. É difícil dizer se o algoritmo de "fusão" para Tipos de Interface resultará em fluxo de controle irredutível.

  • Concorde que esta discussão deve ser movida para problemas no repositório de funclets (ou um novo).
  • Concordo que encontrar um programa que se beneficie dele é difícil de quantificar sem que o LLVM (e Go e outros) realmente emitam o fluxo de controle mais ideal (que pode ser irredutível). A ineficiência causada por FixIrreducibleControlFlow e amigos pode ser um problema de "morte por mil cortes" em um grande binário.
  • Embora eu gostaria de uma implementação apenas de ferramentas como o progresso mínimo absoluto saindo desta discussão, ainda não seria o ideal, já que os produtores agora têm a difícil escolha de usar essa funcionalidade por conveniência (mas depois enfrentam regressões de desempenho imprevisíveis / precipícios), ou fazer o trabalho duro para organizar sua saída para o padrão para que as coisas sejam previsíveis.
  • Se fosse decidido que "gotos" é na melhor das hipóteses um recurso apenas de ferramentas, eu argumentaria que você provavelmente poderia se safar com um recurso ainda mais simples do que o multiloop, já que tudo o que importa é a conveniência do produtor. No mínimo absoluto, um goto <function_byte_offset> seria a única coisa necessária a ser inserida em corpos de função Wasm regulares para permitir que WABT ou Binaryen o transformassem em Wasm legal. Coisas como assinaturas de tipo são úteis se os mecanismos precisarem verificar um multiloop rapidamente, mas se for uma ferramenta de conveniência, também pode torná-lo o máximo conveniente para emitir.

Concordo que encontrar um programa que se beneficie dele é difícil de quantificar sem que o LLVM (e Go e outros) realmente emitam o fluxo de controle mais ideal (que pode ser irredutível).

Concordo que testar em cadeias de ferramentas modificadas + VMs seria o ideal. Mas podemos comparar as compilações wasm atuais com compilações nativas que possuem fluxo de controle ideal. Not So Fast e outros analisaram isso de várias maneiras (contadores de desempenho, investigação direta) e não encontraram fluxo de controle irredutível como um fator significativo.

Mais especificamente, eles não acharam que isso fosse um fator significativo para C/C++. Isso pode ter mais a ver com C/C++ do que com o desempenho do fluxo de controle irredutível. (Eu honestamente não sei.) Parece que @neelance tem motivos para acreditar que o mesmo não seria verdade para Go.

Minha sensação é que existem múltiplas facetas para este problema, e vale a pena enfrentá-lo através de múltiplas direções.

Primeiro, parece que há um problema geral com a capacidade de geração do WebAssembly. Muito disso é causado pela restrição do WebAssembly de ter um binário compacto com verificação de tipo eficiente e compilação de streaming. Poderíamos resolver esse problema, pelo menos em parte, desenvolvendo um "pré"-WebAssembly padronizado que é mais fácil de gerar, mas que é garantido para ser traduzível para "verdadeiro" WebAssembly, idealmente por apenas duplicação de código e inserção de instruções/anotações "apagáveis", com pelo menos alguma ferramenta fornecendo tal tradução.

Em segundo lugar, podemos considerar quais recursos do "pré"-WebAssembly valem a pena incorporar diretamente ao "verdadeiro" WebAssembly. Podemos fazer isso de maneira informada porque teremos módulos "pré"-WebAssembly que podemos analisar antes de serem contorcidos em módulos WebAssembly "verdadeiros".

Alguns anos atrás eu tentei compilar um emulador de bytecode específico para uma linguagem dinâmica (https://github.com/ciao-lang/ciao) para webassembly e o desempenho estava longe de ser o ideal (às vezes 10 vezes mais lento que a versão nativa). O loop de execução principal continha um grande switch de despacho de bytecode, e o mecanismo foi afinado por décadas para ser executado em hardware real, e fazemos uso pesado de rótulos e gotos. Gostaria de saber se esse tipo de software se beneficiaria de suporte para controle de fluxo irredutível ou se o problema era outro. Não tive tempo para fazer uma investigação mais aprofundada, mas ficaria feliz em tentar novamente se as coisas melhorarem. Claro que eu entendo que compilar outras linguagens VM para wasm não é o principal caso de uso, mas seria bom saber se isso será viável, especialmente porque binários universais que rodam eficientemente, em qualquer lugar, é uma das vantagens prometidas de era. (Obrigado e peço desculpas se este tópico em particular foi discutido em alguma outra edição)

@jfmc Meu entendimento é que, se o programa é realista (ou seja, não planejado para ser patológico) e você se preocupa com seu desempenho, então é um caso de uso perfeitamente válido. O WebAssembly pretende ser um bom alvo de uso geral. Então, acho que seria ótimo entender por que você viu uma desaceleração tão significativa. Se isso acontecer devido a restrições no fluxo de controle, seria muito útil saber nesta discussão. Se for devido a outra coisa, ainda seria útil saber como melhorar o WebAssembly em geral.

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

Questões relacionadas

spidoche picture spidoche  ·  4Comentários

nikhedonia picture nikhedonia  ·  7Comentários

thysultan picture thysultan  ·  4Comentários

mfateev picture mfateev  ·  5Comentários

cretz picture cretz  ·  5Comentários