Design: 请支持任意标签和Goto。

创建于 2016-09-08  ·  159评论  ·  资料来源: WebAssembly/design

我想指出我没有参与网络组装工作,
而且我没有维护任何大型或广泛使用的编译器(只是我自己的
玩具般的语言,对 QBE 编译器后端的微小贡献,以及
在 IBM 的编译器团队实习),但我最终变得有点暴躁,并且
被鼓励更广泛地分享。

所以,虽然我有点不舒服跳入并提出重大改变
到一个我没有从事的项目......这里是:

我的投诉:

当我编写编译器时,我首先要做的就是高级别的
结构——循环、if 语句等——验证它们的语义,
做类型检查等等。 我对它们做的第二件事就是扔掉它们
出,并展平为基本块,并可能成为 SSA 形式。 在其他一些地方
在编译器世界中,一种流行的格式是延续传递样式。 我不是
以延续传递风格进行编译的专家,但似乎都没有
非常适合 Web 组装似乎具有的循环和范围块
拥抱。

我想争辩说,基于 goto 的更扁平的格式会更有用,因为
编译器开发人员的目标,并且不会显着阻碍
编写一个可用的 polyfill。

就个人而言,我也不喜欢嵌套复杂的表达式。 他们有点
消耗起来更笨重,特别是如果内部节点可能有副作用,但我
不要强烈反对他们作为编译器实现者——Web 程序集
JIT 可以使用它们,我可以忽略它们并生成映射的指令
给我的 IR。 他们不会让我想翻转桌子。

更大的问题归结为循环、块和其他句法元素
作为优化编译器编写者,您非常努力地表示为
带有代表边的分支的图; 显式控制流构造
是一个障碍。 实际完成后从图表中重建它们
您想要的优化当然是可能的,但它相当多
解决更复杂的格式的复杂性。 这让我很恼火:
生产者和消费者正在解决完全发明的问题
这可以通过简单地删除复杂的控制流结构来避免
从网络组装。

此外,对更高层次结构的坚持导致了一些
病理病例。 例如,达夫的设备以可怕的网络告终
汇编输出,如在The Wasm Explorer 中搞乱
然而,反之则不成立:所有可以表达的东西
在 web assembler 中可以简单地转换为在某些
非结构化,基于 goto 的格式。

所以,至少,我想建议网络组装团队添加
支持任意标签和 goto。 如果他们选择保持较高
级别构造,这会有点浪费的复杂性,但至少
像我这样的编译器编写者将能够忽略它们并生成输出
直接地。

填充:

我在讨论这个问题时听到的一个担忧是循环
和基于块的结构允许更容易地填充网络组件。
虽然这并不完全错误,但我认为一个简单的 polyfill 解决方案
标签和 goto 是可能的。 虽然它可能不是那么理想,
我认为值得在字节码中按顺序进行一点丑陋
避免开始使用内置技术债务的新工具。

如果我们假设 Web 程序集的语法类似于 LLVM(或 QBE),那么一些代码
看起来像:

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

可能编译为:

 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
 }

这可以填充到 Javascript 中,如下所示:

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;
    }
}

丑吗? 是的。 有关系吗? 希望,如果网络组装起飞,
不是很长。

如果不:

好吧,如果我有机会以 Web 程序集为目标,我想我会生成代码
使用我在 polyfill 中提到的方法,并尽我所能忽略所有
高级构造,希望编译器足够聪明
抓住这个模式。

但是,如果我们不需要代码生成的两面,那就太好了
解决指定的格式。

control flow

最有用的评论

即将发布的 Go 1.11 版本将对 WebAssembly 提供实验性支持。 这将包括对 Go 的所有功能的完全支持,包括 goroutines、channels 等。但是,生成的 WebAssembly 的性能目前并不是那么好。

这主要是因为缺少 goto 指令。 如果没有 goto 指令,我们不得不在每个函数中使用顶层循环和跳转表。 使用 relooper 算法对我们来说不是一个选项,因为在 goroutine 之间切换时,我们需要能够在函数的不同点恢复执行。 relooper 对此无能为力,只有 goto 指令可以。

WebAssembly 能够支持像 Go 这样的语言真是太棒了。 但要成为真正的 Web 汇编,WebAssembly 应该与其他汇编语言一样强大。 Go 有一个高级编译器,它能够为许多其他平台发出非常高效的程序集。 这就是为什么我想争辩说,它主要是 WebAssembly 的限制,而不是 Go 编译器的限制,它不可能也使用这个编译器来为 web 发出有效的汇编。

所有159条评论

@oridb Wasm 对消费者进行了一些优化,以便能够快速转换为 SSA 形式,并且该结构确实有助于常见代码模式,因此该结构不一定是消费者的负担。 我不同意您的断言“代码生成的双方都围绕指定的格式工作”。 Wasm 在很大程度上是关于一个苗条和快速的消费者,如果你有一些建议让它更苗条和更快,那么这可能是建设性的。

可以排序到 DAG 中的块可以在 wasm 块和分支中表示,例如您的示例。 switch-loop 是必要时使用的样式,也许消费者可能会在这里做一些跳转线程来提供帮助。 也许看看 binaryen 可能会为您的编译器后端做很多工作。

还有其他要求提供更通用的 CFG 支持,以及提到的其他一些使用循环的方法,但目前的重点可能在其他地方。

我不认为有任何计划在编码中明确支持“继续传递样式”,但是已经提到块和循环弹出参数(就像 lambda)和支持多个值(多个 lambda 参数)并添加一个pick运算符使引用定义(lambda 参数)更容易。

该结构确实有助于常见的代码模式

我没有看到任何常见的代码模式更容易表示为任意标签的分支,而不是 Web 程序集强制执行的受限循环和块子集。 如果尝试使代码与某些语言类的输入代码非常相似,我可以看到一个小好处,但这似乎不是目标——如果它们在那里,构造有点裸露

可以排序到 DAG 中的块可以在 wasm 块和分支中表示,例如您的示例。

是的,他们可以。 但是,我强烈不希望添加额外的工作来确定哪些可以以这种方式表示,哪些需要额外的工作。 实际上,我会跳过做额外的分析,总是只生成开关循环形式。

同样,我的论点不是循环和块使事情变得不可能。 他们可以做的所有事情都让机器用 goto、goto_if 和任意的非结构化标签来编写更简单、更容易。

也许看看 binaryen 可能会为您的编译器后端做很多工作。

我已经有一个我相当满意的可维护后端,并计划用我自己的语言完全引导整个编译器。 我不想仅仅为了解决循环/块的强制使用而添加一个相当大的额外依赖项。 如果我只是使用 switch 循环,那么发出代码是非常简单的。 如果我尝试有效地实际使用 Web 组装中存在的功能,而不是尽我最大的努力假装它们不存在,它会变得更加不愉快。

还有其他要求更一般的 CFG 支持的请求,以及提到的使用循环的其他一些方法,但目前力量可能在其他地方。

我仍然不相信循环有什么好处——任何可以用循环表示的东西都可以用 goto 和标签表示,并且从平面指令列表到 SSA 的快速且众所周知的转换。

就 CPS 而言,我认为不需要明确的支持——它在 FP 圈子中很流行,因为它很容易直接转换为汇编,并且在推理方面给 SSA 带来了类似的好处(http:// mlton.org/pipermail/mlton/2003-January/023054.html); 再说一次,我不是这方面的专家,但据我记得,调用延续被降低到一个标签、几个 mov 和一个 goto。

@oridb '从平面指令列表到 SSA 的快速且众所周知的转换'

知道它们如何与 wasm SSA 解码器进行比较会很有趣,这是重要的问题吗?

Wasm 目前使用值堆栈,如果没有这种结构,其中的一些好处就会消失,它会损害解码器的性能。 如果没有值堆栈,SSA 解码也会有更多的工作,我尝试了一个寄存器基本代码并且解码速度较慢(不确定这有多重要)。

您会保留值堆栈,还是使用基于寄存器的设计? 如果保留值堆栈,那么它可能会成为 CIL 克隆,也许 wasm 性能可以与 CIL 进行比较,有没有人真正检查过这个?

您会保留值堆栈,还是使用基于寄存器的设计?

在这方面,我实际上并没有任何强烈的感觉。 我想编码的紧凑性将是最大的担忧之一。 寄存器设计在那里可能不会那么好 - 或者它可能会通过 gzip 压缩得非常好。 我真的不知道我的头顶。

性能是另一个问题,尽管我怀疑考虑到缓存二进制输出的能力,它可能不太重要,而且下载时间可能比解码时间长几个数量级。

知道它们如何与 wasm SSA 解码器进行比较会很有趣,这是重要的问题吗?

如果您要解码为 SSA,这意味着您还将进行合理数量的优化。 我很想首先对解码性能的显着性进行基准测试。 但是,是的,这绝对是一个好问题。

感谢您的问题和疑虑。

值得注意的是,许多设计者和实现者
WebAssembly 具有高性能、工业 JIT 的背景,不仅
适用于 JavaScript(V8、SpiderMonkey、Chakra 和 JavaScriptCore),但也适用于
LLVM 和其他编译器。 我个人已经为 Java 实现了两个 JIT
字节码,我可以证明具有不受限制的 goto 的堆栈机器
在解码、验证和构造一个
编译器 IR。 其实Java可以表达的模式有很多
将导致高性能 JIT 的字节码,包括 C1 和 C2
HotSpot 简单地放弃并将代码降级为仅在
口译员。 相反,从类似的东西构造编译器 IR
来自 JavaScript 或其他语言的 AST 也是我做过的事情。 这
AST 的额外结构使其中一些工作变得更加简单。

WebAssembly 的控制流结构的设计通过以下方式简化了消费者
实现快速、简单的验证、简单的一次性转换为 SSA 表格
(甚至是图 IR)、有效的单通道 JIT 和(带有后序和
stack machine)相对简单的就地解释。 结构化的
control 使不可约控制流图成为不可能,从而消除了
解码器和编译器的一整类讨厌的角落案例。 它也是
很好地为 WASM 字节码中的异常处理奠定了基础,V8
已经在开发与生产一致的原型
执行。

我们在成员之间就这个问题进行了很多内部讨论
主题,因为对于字节码,它是最不同的一件事
其他机器级目标。 但是,它与定位没有什么不同
像 JavaScript 这样的源语言(现在许多编译器都这样做)和
只需对块进行少量重组即可实现结构。 那里
是已知的算法和工具。 我们想提供一些
为那些从任意 CFG 开始的生产者提供更好的指导
更好地沟通这一点。 对于直接从 AST 定位 WASM 的语言
(这实际上是 V8 现在对 asm.js 代码所做的事情——直接
将 JavaScript AST 转换为 WASM 字节码),没有重组
必要的步骤。 我们希望许多语言工具都是这种情况
整个光谱内部没有复杂的 IR。

2016 年 9 月 8 日星期四上午 9:53,Ori Bernstein [email protected]
写道:

您会保留值堆栈,还是使用基于寄存器的设计?

在这方面,我实际上并没有任何强烈的感觉。 我会想象
编码的紧凑性将是最大的担忧之一; 正如你
提到,性能是另一回事。

知道它们如何与 wasm SSA 解码器进行比较会很有趣,
是重要的问题吗?

如果您要解码到 SSA,这意味着您也要做一个
合理的优化量。 我很好奇如何进行基准测试
显着的解码性能是第一位的。 但是,是的,就是这样
绝对是个好问题。


您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/WebAssembly/design/issues/796#issuecomment -245521009,
或使线程静音
https://github.com/notifications/unsubscribe-auth/ALnq1Iz1nn4--NL32R9ev0JPKfEnDyvqks5qn77cgaJpZM4J3ofA
.

谢谢@titzer ,我怀疑 Wasm 的结构除了与 asm.js 相似之外还有其他用途。 不过我想知道:Java 字节码(和 CIL)不直接对 CFG 或值堆栈建模,它们必须由 JIT 推断。 但是在 Wasm 中(特别是如果添加了块签名),JIT 可以很容易地弄清楚值堆栈和控制流发生了什么,所以我想知道,如果 CFG(或特别是不可约控制流)像循环和块一样被显式建模,这是否可以避免您正在考虑的大多数令人讨厌的极端情况?

解释器使用这种巧妙的优化,它依赖于不可约控制流来改进分支预测......

@oridb

我想争辩说,基于 goto 的更扁平的格式会更有用,因为
编译器开发人员的目标

我同意 goto 对许多编译器非常有用。 这就是为什么像 Binaryen 这样的工具可以让您使用 gotos生成任意 CFG ,并且它们可以非常快速有效地为您将其转换为 WebAssembly。

将 WebAssembly 视为一种针对浏览器使用而优化的东西可能会有所帮助(正如@titzer指出的那样)。 大多数编译器可能不应该直接生成 WebAssembly,而是使用像 Binaryen 这样的工具,这样他们就可以发出 goto,免费获得一堆优化,并且不需要考虑 WebAssembly 的低级二进制格式细节(而是您使用简单的 API 发出 IR)。

关于使用您提到的 while-switch 模式进行 polyfilling:在 emscripten 中,我们在开发重新创建循环的“relooper”方法之前就以这种方式开始了。 while-switch 模式平均慢了大约 4 倍(但在某些情况下明显更少或更多,例如小循环更敏感)。 我同意你的观点,理论上跳线程优化可以加快速度,但性能将难以预测,因为某些 VM 会比其他 VM 做得更好。 它在代码大小方面也明显更大。

将 WebAssembly 视为一种针对浏览器使用而优化的东西可能会有所帮助(正如@titzer指出的那样)。 大多数编译器可能不应该直接生成 WebAssembly,而是使用像 Binaryen 这样的工具......

我仍然不相信这方面会那么重要 - 再次,我怀疑获取字节码的成本将主导用户看到的延迟,第二大成本是完成的优化,而不是解析和验证. 我还假设/希望字节码会被丢弃,而编译后的输出将被缓存,从而使编译有效地成为一次性成本。

但是,如果您正在针对 Web 浏览器的使用进行优化,为什么不简单地将 Web 程序集定义为 SSA,在我看来,这既符合我的预期,又可以减少“转换”为 SSA 的努力?

您可以在下载时开始解析和编译,并且某些 VM 可能不会预先进行完整编译(例如,它们可能只使用简单的基线)。 因此,下载和编译时间可能比预期的要短,因此解析和验证最终会成为用户看到的总延迟的一个重要因素。

关于 SSA 表示,它们往往具有较大的代码大小。 SSA 非常适合优化代码,但不适用于紧凑地序列化代码。

@oridb“通过实现快速,简单的验证,方便,一个单程转化SSA形式... WebAssembly的控制流结构简化了消费者设计”见@titzer注释-它可以一次生成_verified_ SSA。 即使 wasm 使用 SSA 进行编码,它仍然有验证它的负担,计算支配结构,这很容易使用 wasm 控制流限制。

wasm 的大部分编码效率似乎来自针对通用代码模式的优化,其中定义具有按堆栈顺序使用的单一用途。 我希望 SSA 编码也可以这样做,因此它可能具有相似的编码效率。 用于菱形图案的if_else等运算符也有很大帮助。 但是如果没有 wasm 结构,看起来所有基本块都需要从寄存器读取定义并将结果写入寄存器,这可能不会那么有效。 例如,我认为 wasm 可以使用pick运算符做得更好,该运算符可以在堆栈中引用作用域堆栈值并跨越基本块边界。

我认为 wasm 距离能够以 SSA 样式编码大多数代码并不远。 如果定义作为基本块输出向上传递范围树,那么它可能是完整的。 SSA 编码可能与 CFG 问题正交。 例如,可能存在带有 wasm CFG 限制的 SSA 编码,可能存在带有 CFG 限制的基于寄存器的 VM。

wasm 的目标是将优化负担移出运行时使用者。 在运行时编译器中增加复杂性有很强的抵抗力,因为它会增加攻击面。 如此多的设计挑战是询问可以做些什么来简化运行时编译器而不损害性能,以及很多争论!

好吧,现在可能为时已晚,但我想质疑 relooper 算法或其变体在所有情况下都能产生“足够好”的结果的想法。 在大多数情况下,它们显然可以,因为大多数源代码开始时不包含不可简化的控制流,优化通常不会使事情变得过于复杂,如果它们这样做,例如作为合并重复块的一部分,它们可能会被教导不要。 但是病理病例呢? 例如,如果您有一个协程,编译器已将其转换为具有如下伪 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
}

所以你有大部分正常的控制流,但有一些 goto 指向它的中间。 这大致就是 LLVM 协程的工作方式

如果“正常”控制流足够复杂,我认为没有什么好方法可以重新循环类似的东西。 (可能是错的。)要么复制大部分功能,可能需要为每个屈服点单独复制,要么将整个东西变成一个巨大的开关,根据@kripken ,它比典型代码上的

虚拟机可以通过跳转线程优化来减少巨型切换的开销,但虚拟机执行这些优化肯定会更昂贵,本质上是猜测代码如何减少到 goto,而不是仅仅接受显式 goto。 正如@kripken所说,它也不太可预测。

也许一开始就进行这种转换是一个坏主意,因为之后没有任何东西可以支配任何事情,所以基于 SSA 的优化不能做太多……也许在汇编级别做得更好,也许 wasm 最终应该获得原生协程支持? 但是编译器可以在进行转换之前执行大部分优化,而且似乎至少 LLVM 协程的设计者没有看到迫切需要将转换延迟到代码生成。 另一方面,由于人们想要从协程中获得的确切语义有相当多的变化(例如,暂停协程的重复,检查 GC 的“堆栈帧”的能力),当涉及到设计可移植字节码时(而不是编译器),正确支持已经转换的代码比让 VM 进行转换更灵活。

无论如何,协程只是一个例子。 我能想到的另一个例子是实现一个 VM-within-a-VM。 虽然 JIT 的一个更常见的特性是 side exits ,它不需要 goto,但在某些情况下需要 side entry - 再次,需要 goto 进入循环中间等。 另一个是优化的解释器:并不是说以 wasm 为目标的解释器可以真正匹配那些以本机代码为目标的解释器,这至少可以提高计算 goto 的性能,并且可以深入到汇编中以获得更多……但计算 goto 的部分动机是更好地利用通过为每个案例提供自己的跳转指令来分支预测器,因此您可以通过在每个操作码处理程序之后进行单独的切换来复制一些效果,其中案例都只是 goto。 或者至少有一个 if 或两个来检查通常出现在当前指令之后的特定指令。 该模式的一些特殊情况可以用结构化控制流来表示,但不是一般情况。 等等…

当然,有一些方法可以允许任意控制流,而无需让 VM 做很多工作。 稻草人的想法,可能会被打破:您可以有一个允许跳转到子范围的方案,但前提是您必须输入的范围数量小于目标块定义的限制。 限制将默认为 0(不从父范围跳转),这会保留当前语义,并且块的限制不能大于父块的限制 + 1(易于检查)。 并且 VM 会将其优势启发式从“如果 X 是 Y 的父级,则 X 支配 Y”更改为“如果它是距离大于 Y 的子跳跃限制的 Y 的父级,则 X 支配 Y”。 (这是一个保守的近似值,不能保证代表精确的支配集,但对于现有的启发式方法也是如此 - 内部块有可能支配外部块的下半部分。)因为只有具有不可约控制流的代码需要指定一个限制,在常见情况下它不会增加代码大小。

编辑:有趣的是,这基本上会使块结构成为优势树的表示。 我想直接表达它会简单得多:基本块的树,其中允许一个块跳转到兄弟、祖先或直接子块,但不能跳转到更远的后代。 我不确定如何最好地映射到现有的范围结构,其中一个“块”可以由多个基本块组成,中间有子循环。

FWIW:Wasm 有一个特殊的设计,仅用几个非常重要的词来解释“除了嵌套限制使得无法从循环外部分支到循环中间”。

如果它只是一个 DAG,那么验证可以只检查分支是否向前,但是对于循环,这将允许从循环外部分支到循环的中间,因此是嵌套块设计。

CFG 只是此设计的一部分,另一个是数据流,还有一堆值和块也可以组织以展开值堆栈,这可以非常有用地将生存范围传达给消费者,从而节省转换为 SSA 的工作.

可以将 wasm 扩展为 SSA 编码(添加pick ,允许块返回多个值,并让循环条目弹出值),因此有趣的是,可能不需要高效 SSA 解码所需的约束(因为它可能已经是 SSA 编码的)! 这导致了一种函数式语言(为了提高效率,它可能具有堆栈样式编码)。

如果将其扩展为处理任意 CFG,那么它可能如下所示。 这是一种 SSA 样式编码,因此值是常量。 它似乎在很大程度上仍然适合堆栈样式,只是不确定所有细节。 因此,在blocks可以对该集合中的任何其他标记块进行分支,或者用于将控制权转移到另一个块的其他约定。 块中的代码可能仍然有用地引用堆栈更高的值堆栈上的值,以节省将它们全部传入。

(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)
    ...
   ))

但是网络浏览器会在内部处理这种高效吗?

具有堆栈机器背景的人会识别代码模式并能够将其与堆栈编码相匹配吗?

这里有一些关于不可约循环的有趣讨论http://bboissin.appspot.com/static/upload/bboissin-thesis-2010-09-22.pdf

我没有快速通过它,但它提到通过添加入口节点将不可约循环转换为可约循环。 对于 wasm 来说,这听起来像是在循环中添加一个定义的输入,专门用于在循环内调度,类似于当前的解决方案,但为此定义了一个变量。 上面提到这是在处理中被虚拟化、优化掉的。 也许这样的事情可能是一种选择?

如果这即将到来,并且鉴于生产者已经需要使用类似的技术但使用局部变量,那么现在是否值得考虑让早期生产的 wasm 有可能在更高级的运行时运行得更快? 这也可能会激发运行时之间的竞争来探索这一点。

这不完全是任意的标签和 goto,而是这些可能被转换成的东西,将来有可能被有效地编译。

郑重声明,我强烈支持@oridb@comex在这个问题上。
我认为这是一个关键问题,应该在为时已晚之前解决。

鉴于 WebAssembly 的性质,您现在犯的任何错误都可能会持续数十年(看看 Javascript!)。 这就是问题如此关键的原因。 无论出于何种原因,现在都避免支持 goto(例如,为了简化优化,坦率地说,具体实现对通用事物的影响,老实说,我认为它很懒),你最终会得到长期存在的问题。

我已经可以看到未来(或当前,但在未来)的 WebAssembly 实现试图以特殊情况识别通常的 while/switch 模式来实现标签,以便正确处理它们。 这是一个黑客。

WebAssembly 是白纸黑字,所以现在是时候避免肮脏的 hack(或者更确切地说,是对它们的要求)。

@darkuranium

当前指定的 WebAssembly 已经在浏览器和工具链中发布,并且开发人员已经创建了采用该设计中布局的形式的代码。 因此,我们不能以破坏性的方式改变设计。

但是,我们可以以向后兼容的方式添加到设计中。 我不认为任何相关人员认为goto是无用的。 我怀疑我们都经常使用goto ,而不仅仅是句法玩具。

在这个时间点,有动力的人需要提出一个有意义的建议并实施。 如果提供可靠的数据,我认为这样的提议不会被拒绝。

鉴于 WebAssembly 的性质,您现在犯的任何错误都可能会持续数十年(看看 Javascript!)。 这就是问题如此关键的原因。 无论出于何种原因,现在都避免支持 goto(例如,为了简化优化,坦率地说,具体实现对通用事物的影响,老实说,我认为它很懒),你最终会得到长期存在的问题。

所以我认为你是虚张声势:我认为你表现出的动机,而不是像我上面详述的那样提出建议和实施,坦率地说是很懒惰的。

我当然是厚脸皮。 考虑到我们已经有人敲门寻找线程、GC、SIMD 等——他们都在为为什么他们的功能最重要提出热情而明智的论据——如果你能帮助我们解决其中一个问题,那就太好了。 有些人这样做是为了我提到的其他功能。 到目前为止, goto没有。 请熟悉该小组的贡献指南并加入其中。

否则我认为goto是一个很棒的未来功能。 就我个人而言,我可能会首先解决其他问题,例如 JIT 代码生成。 这是我在 GC 和线程之后的个人兴趣。

你好。 我正在编写从 webassembly 到 IR 并返回到 webassembly 的翻译,我已经与人们讨论过这个主题。

有人指出,不可约控制流很难在 webassembly 中表示。 事实证明,对于优化偶尔写出不可约控制流的编译器来说是很麻烦的。 这可能类似于下面的循环,它有多个入口点:

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

EBB 编译器将生成以下内容:

entry:
    cjump x, inside_loop
    // banana
    jump loop

loop:
    cjump y, exit
    // things
    jump inside_loop

inside_loop:
    // do things
    jump loop
exit:
    return

接下来我们将其转换为 webassembly。 问题是,尽管我们早在很久以前就发现了

在它被翻译之前,编译器将对此进行处理。 但最终你可以扫描代码并定位结构的开头和结尾。 消除掉路跳跃后,您最终得到以下候选人:

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

接下来,您需要从这些中构建一个堆栈。 哪一个走到底? 它要么是“内部循环”,要么是“循环”。 我们不能这样做,所以我们必须削减堆栈并复制周围的东西:

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

现在我们可以把它翻译成 webassembly。 请原谅我,我还不熟悉这些循环是如何构建的。

如果我们考虑旧软件,这不是一个特别的问题。 新软件很可能被翻译成网络汇编。 但问题在于我们的编译器是如何工作的。 他们一直在用 _decades_ 的基本块进行控制流,并假设一切正常。

从技术上讲,语言先被翻译,然后再翻译出来。 我们只需要一种机制,允许价值观在没有戏剧性的情况下整洁地跨越边界。 结构化流程仅对打算阅读代码的人有用。

但是例如,以下内容也可以正常工作:

    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.

这些数字是隐含的,也就是说.. 当编译器看到一个“标签”时,它知道它开始了一个新的扩展块并给它一个新的索引号,从 0 开始递增。

要生成静态堆栈,您可以在遇到跳转到标签时跟踪堆栈中有多少项目。 如果跳转到标签后最终出现不一致的堆栈,则程序无效。

如果您发现上述情况不好,您还可以尝试在每个标签中添加显式堆栈长度(如果绝对值不利于压缩,则可能是最后一个索引标签的堆栈大小的增量),并在每次跳转时添加一个关于多少值的标记它在跳转期间从堆栈顶部复制。

我敢打赌,就你如何表示控制流而言,你无法以任何方式超越 gzip,所以你可以选择对这里工作最辛苦的人来说很好的流。 (我可以用我灵活的编译器工具链来说明“智取 gzip”——如果你愿意,只需给我发消息,然后放一个演示!)

我现在感觉自己像个疯子。 只是重新阅读了 WebAssembly 规范并发现不可简化的控制流被故意从 MVP 中排除,这可能是 emscripten 必须在早期解决问题的原因。

在“Emscripten: An LLVM-to-JavaScript Compiler”一文中解释了如何处理 WebAssembly 中不可约控制流的解决方案。 relooper 重新组织程序,如下所示:

_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

合理的是结构化控制流有助于阅读源代码转储,我猜它被认为有助于 polyfill 实现。

从 webassembly 编译的人可能会适应处理和分离折叠的控制流。

所以:

  • 如前所述,WebAssembly 现在是稳定的,因此完全重写控制流的表达方式的时间已经过去。

    • 从某种意义上说,这是不幸的,因为没有人真正测试过更直接的基于 SSA 的编码是否可以实现与当前设计相同的紧凑性。

    • 但是,当涉及到指定 goto 时,这会使工作变得更容易! 基于块的指令已经超越了循环,期望以 wasm 为目标的生产编译器使用它们来表达可简化的控制流并不是什么大不了的事——算法并不难。 主要问题是在没有性能成本的情况下,无法使用它们来表达一小部分控制流。 如果我们通过添加一个新的 goto 指令来解决这个问题,我们不必像完全重新设计那样担心编码效率。 当然,使用 goto 的代码应该仍然相当紧凑,但它不必与其他结构竞争紧凑性; 它仅用于不可约控制流,应很少使用。

  • 可还原性不是特别有用。

    • 大多数编译器后端使用基于基本块和它们之间的分支图的 SSA 表示。 嵌套循环结构,可还原性保证的东西,一开始就被扔掉了。

    • 我检查了 JavaScriptCore、V8 和 SpiderMonkey 中当前的 WebAssembly 实现,它们似乎都遵循这种模式。 (V8 更复杂——某种“节点海”表示而不是基本块——但也抛弃了嵌套结构。)

    • 例外:循环分析很有用,所有这三个实现都将信息传递给 IR,说明哪些基本块是循环的开始。 (与 LLVM 相比,LLVM 作为为 AOT 编译而设计的“重量级”后端,将其丢弃并在后端重新计算。这更健壮,因为它可以在源代码中找到看起来不像循环但确实经过一系列优化,但速度较慢。)

    • 循环分析适用于“自然循环”,它禁止分支到不通过循环头的循环中间。

    • WebAssembly 应该继续保证loop块是自然循环。

    • 但是循环分析不需要整个函数是可约的,甚至循环内部也不需要:它只是禁止从外到内的分支。 基本表示仍然是任意控制流图。

    • 不可约化的控制流确实使得将 WebAssembly 编译为 JavaScript(polyfilling)变得更加困难,因为编译器必须自己运行 relooper 算法。

    • 但是 WebAssembly 已经做出了多项决定,这些决定为任何 compile-to-JS 方法增加了显着的运行时开销(包括未对齐的内存访问支持和越界访问陷阱),这表明它并不被认为非常重要。

    • 与此相比,让编译器稍微复杂一点并不是什么大问题。

    • 因此,我认为没有充分的理由不为不可约控制流添加某种支持。

  • 构建 SSA 表示所需的主要信息(根据设计,应该可以一次性完成)是支配树

    • 目前,后端可以根据结构化控制流来估计优势。 如果我正确理解规范,以下说明将结束一个基本块:

    • block



      • 开始区块的 BB 由前一个 BB 主导。 *


      • 对应的end后面的BB由开始区块的BB支配,而不是由end之前的BB支配(因为如果有br出来,它将被跳过)。



    • loop



      • 开始区块的 BB 由前一个 BB 控制。


      • end的 BB 由end之前的 BB 支配(因为除了执行end之外,您无法进入end之后的指令)。



    • if



      • if 端、else 端和end之后的 BB 都被if之前的 BB 支配。



    • brreturnunreachable



      • (紧跟在brreturnunreachable之后的 BB 是不可达的。)



    • br_if , br_table :



      • br_if / br_table之前的BB 支配它之后的BB。



    • 值得注意的是,这只是一个估计。 它不会产生误报(说 A 支配 B 而实际上没有),因为它只是在没有通过 A 的情况下无法通过构造到达 B 时才这么说。 但它可能会产生假阴性(说 A 在实际发生时不会支配 B),而且我认为单通道算法无法检测到这些(可能是错误的)。

    • 假阴性示例:

      ```

      阻止$外部

      环形

      br $外部;; 既然这个无条件破了,就暗中主宰端BB

      结尾

      结尾

    • 但没关系,AFAIK。



      • 误报是不好的,因为例如,如果说基本块 A 支配基本块 B,则 B 的机器代码可以使用 A 中设置的寄存器(如果中间没有覆盖该寄存器)。 如果 A 没有真正支配 B,那么寄存器可能有一个垃圾值。


      • 假阴性本质上是永远不会出现的幽灵分支。 编译器假设这些分支可能发生,但不是必须发生,因此生成的代码只是比必要的更保守。



    • 无论如何,考虑一下goto指令在支配树方面应该如何工作。 假设 A 支配 B,而 B 支配 C。

    • 我们不能从 A 跳到 C,因为那会跳过 B(违反优势假设)。 换句话说,我们不能跳转到非直接后代。 (而在二元生产者端,如果他们计算出真正的支配树,就永远不会有这样的跳跃。)

    • 我们可以安全地从 A 跳转到 B,但是转到直接后代并没有那么有用。 这基本上相当于一个if或switch语句中,我们已经可以做到(使用if指令只要有一个二进制测试,或br_table如果有多个)。

    • 同样安全,更有趣的是,跳转到兄弟姐妹或祖先的兄弟姐妹。 如果我们跳到我们的兄弟姐妹,我们保留了我们的父母支配我们的兄弟姐妹的保证,因为我们必须已经执行了我们的父母才能到达这里(因为它也支配了我们)。 对于祖先也是如此。

    • 一般来说,恶意二进制文件可能会以这种方式产生占主导地位的假阴性,但正如我所说,这些是 (a) 已经可能的并且 (b) 可以接受的。

  • 基于此,这是一个稻草人建议:

    • 一种新的块类型指令:
    • 标签resultTypeÑINSTR *端
    • 必须有恰好 N 个直接子指令,其中“直接子”是指块类型指令( loopblocklabels )以及对应的所有指令end或单个非块指令(不得影响堆栈)。
    • labels不像其他块类型指令那样创建单个标签,而是创建 N+1 个标签:N 指向 N 个孩子,一个指向labels块的末尾。 在每个孩子中,标签索引 0 到 N-1 按顺序指代孩子,标签索引 N 指结尾。

    换句话说,如果你有
    loop ;; outer labels 3 block ;; child 0 br X end nop ;; child 1 nop ;; child 2 end end

    根据 X, br指的是:

    | X | 目标 |
    | ---------- | ------ |
    | 0 | block |
    | 1 | 孩子 0( block开头)|
    | 2 | 孩子 1 (nop) |
    | 3 | 孩子 2 (nop) |
    | 4 | labels |
    | 5 | 外循环开始 |

    • 执行从第一个孩子开始。

    • 如果执行到达其中一个孩子的末尾,则继续执行下一个。 如果它到达最后一个孩子的末尾,它会回到第一个孩子。 (这是为了对称,因为孩子的顺序并不重要。)

    • 分支到其中一个孩子将操作数堆栈展开到labels开头的深度。

    • 分支到末尾也是如此,但如果结果类型为非空,则分支到末尾会弹出一个操作数并在展开后将其推送,类似于block

    • 支配: labels指令之前的基本块支配每个孩子,以及labels结束后的BB。 孩子们不互相支配或结束。

    • 设计说明:

    • N 是预先指定的,以便可以一次性验证代码。 在知道其中索引的目标之前,必须到达labels块的末尾才能知道孩子的数量是很奇怪的。

    • 不确定最终是否应该有一种方法可以在标签之间的操作数堆栈上传递值,但类似于无法将值传递到blockloop ,这可能是不支持开始的和。

不过,如果可以跳入循环,那就太好了,不是吗? IIUC,如果考虑到这种情况,那么讨厌的循环+ br_table 组合将永远不需要......

编辑:哦,您可以通过在labels向上跳跃来制作没有loop的循环。 不敢相信我错过了。

@qwertie如果给定循环不是自然循环,则以 wasm 为目标的编译器应使用labels而不是loop来表达它。 如果这就是您所指的,则永远不需要添加开关来表达控制流。 (毕竟,在最坏的情况下,你可以只为函数中的每个基本块使用一个带有标签的巨型labels块。这不会让编译器知道优势和自然循环,所以你可能会错过优化。但labels仅在这些优化不适用的情况下才需要。)

嵌套循环结构,可还原性保证的东西,一开始就被扔掉了。 [...] 我检查了 JavaScriptCore、V8 和 SpiderMonkey 中当前的 WebAssembly 实现,它们似乎都遵循这种模式。

不完全是:至少在 SM 中,IR 图不是一个完全一般的图; 我们假设某些图形不变量是从结构化源(JS 或 wasm)生成的,并且通常会简化和/或优化算法。 支持一个完全通用的 CFG 要么需要审核/更改管道中的许多通道以不假设这些不变量(通过泛化它们或在不可约的情况下对其进行悲观)或预先进行节点分割复制以使图可约化。 当然,这当然是可行的,但是这仅仅是因为wasm 成为人为瓶颈的问题是不正确的。

此外,有许多选项并且不同的引擎会做不同的事情这一事实表明,让生产者预先处理不可约性将在存在不可约控制流的情况下产生更可预测的性能。

当我们过去讨论通过任意 goto 支持扩展 wasm 的向后兼容路径时,一个大问题是这里的用例是什么:它是“通过不必运行 relooper 类型的算法使生产者更简单”还是它“允许更有效的代码生成用于实际不可约化的控制流”? 如果只是前者,那么我认为我们可能需要一些嵌入任意标签/goto 的方案(既向后兼容,也与未来的块结构 try/catch 组合); 这只是权衡成本/收益和上述问题的问题。

但是对于后一个用例,我们观察到的一件事是,当您时不时地在野外看到 Duff 的设备案例时(这实际上不是展开循环的有效方法......),通常你看到不可约性弹出的地方,性能很重要的是解释器循环。 解释器循环也受益于需要计算 goto 的间接线程。 此外,即使在功能强大的离线编译器中,解释器循环也往往会获得最差的寄存器分配。 由于解释器循环性能可能非常重要,一个问题是我们是否真正需要一种控制流原语,它允许引擎执行间接线程并执行体面的 regalloc。 (这对我来说是一个悬而未决的问题。)

@lukewagner
我想了解有关哪些通行证取决于不变量的更多详细信息。 我提出的设计,对不可约流使用单独的构造,应该使像 LCM 这样的优化通道相对容易避开该流。 但如果还有其他我没有想到的破损类型,我想更好地了解它们的性质,以便更好地了解是否以及如何避免它们。

当我们过去讨论通过任意 goto 支持扩展 wasm 的向后兼容路径时,一个大问题是这里的用例是什么:它是“通过不必运行 relooper 类型的算法使生产者更简单”还是它“允许更有效的代码生成用于实际不可约化的控制流”?

对我来说是后者。 我的建议希望生产者仍然运行一个 relooper 类型的算法来节省后端识别支配者和自然循环的工作,只有在必要时才回退到labels 。 但是,这仍然会使生产者变得更简单。 如果不可约控制流有很大的惩罚,一个理想的生产者应该非常努力地避免它,使用启发式方法来确定复制代码是否更有效,可以工作的最小复制量等。如果唯一的惩罚是潜在地给出向上循环优化,这并不是真正必要的,或者至少与使用常规机器代码后端(具有自己的循环优化)相比没有必要。

我真的应该收集更多关于不可约控制流在实践中的常见程度的数据……

然而,我认为惩罚这种流动本质上是武断的和不必要的。 在大多数情况下,对整个程序运行时间的影响应该很小。 但是,如果热点恰好包含不可约控制流,则会受到严厉惩罚; 将来,WebAssembly 优化指南可能会将此作为常见问题,并解释如何识别和避免它。 如果我的看法是正确的,那么这对程序员来说是一种完全不必要的认知开销形式。 即使开销很小,与本机代码相比,WebAssembly 已经有足够的开销,它应该设法避免任何额外的开销。

我愿意说服我的信念是不正确的。

由于解释器循环性能可能非常重要,一个问题是我们是否真正需要一种控制流原语,它允许引擎执行间接线程并执行体面的 regalloc。

这听起来很有趣,但我认为从更通用的原语开始会更好。 毕竟,为解释器量身定制的原语仍然需要后端来处理不可约的控制流; 如果您要咬紧牙关,也不妨支持一般情况。

或者,我的提议可能已经成为口译员的一个不错的原语。 如果将labelsbr_table ,则可以将跳转表直接指向函数中的任意点,这与计算的 goto 没有什么不同。 (与 C 开关相反,它至少最初将控制流引导到开关块内的点;如果情况都是 goto,编译器应该能够优化掉额外的跳转,但它也可能合并多个“冗余” switch 语句合二为一,破坏了在每个指令处理程序之后进行单独跳转的好处。)虽然我不确定寄存器分配的问题是什么......

@comex我想在存在不可约控制流的情况下,可以简单地关闭函数级别的整个优化通道(尽管可能需要 SSA 生成、regalloc 和其他一些,因此需要工作),但我假设我们想要为具有不可约控制流的函数实际生成质量代码,这涉及审计以前假定结构化图的每个算法。

>

可还原性保证的嵌套循环结构是
一开始就被扔掉了。 [...]我检查了当前
JavaScriptCore、V8 和 SpiderMonkey 中的 WebAssembly 实现,以及
他们似乎都遵循这种模式。

不完全是:至少在 SM 中,IR 图不是一个完全一般的图; 我们
假设某些图不变量是从一个生成的
结构化源代码(JS 或 wasm),并经常简化和/或优化
算法。

在 V8 中也是如此。 这实际上是我对 SSA 的主要抱怨之一
他们几乎从未定义过的各自的文献和实现
什么构成了“格式良好”的 CFG,但倾向于隐含地假设各种
无论如何,未记录的约束,通常由
语言前端。 我敢打赌,现有编译器中的许多/大多数优化
将无法处理真正任意的 CFG。

正如@lukewagner所说,不可约控制的主要用例可能是
优化解释器的“线程代码”。 很难说这些有多相关
是针对 Wasm 域的,它的缺席是否真的是最大的
瓶颈。

与许多人讨论了不可约控制流
研究编译器 IR,“最干净”的解决方案可能是添加
相互递归块的概念。 这恰好适合Wasm的
控制结构相当好。

LLVM 中的循环优化通常会忽略不可约控制流,并且不会尝试对其进行优化。 他们所基于的循环分析只会识别自然循环,因此您只需要注意可能存在不被识别为循环的 CFG 循环。 当然,其他优化本质上更局部,并且与不可约 CFG 一起工作得很好。

根据记忆,可能是错误的,SPEC2006 在 401.bzip2 中有一个不可约循环,仅此而已。 这在实践中相当罕见。

Clang 只会在使用计算 goto 的函数中发出一条indirectbr指令。 这具有将线程解释器变成自然循环的效果,其中indirectbr块作为循环头。 离开 LLVM IR 后,单个indirectbr在代码生成器中进行尾部复制以重建原始缠结。

不可约控制流没有单通验证算法
我知道。 仅可简化控制流的设计选择是
受此要求影响较大。

如前所述,不可约控制流至少可以建模两个
不同的方式。 带有 switch 语句的循环实际上可以优化
通过简单的局部跳转线程进入原始不可约图
优化(例如,通过折叠模式,其中分配一个常数
到局部变量,然后分支到条件分支
立即打开该局部变量)。

因此,不可约控制结构根本没有必要,它是
只需一个编译器后端转换即可恢复
原始不可约图并对其进行优化(对于其编译器的引擎
支持不可简化的控制流——这 4 个浏览器都不支持
据我所知)。

最好的,
-本

2017 年 4 月 20 日星期四上午 5:20,Jakob Stoklund Olesen <
通知@github.com> 写道:

LLVM 中的循环优化通常会忽略不可约控制流
而不是试图优化它。 他们基于的循环分析将
只识别自然循环,所以你只需要意识到可以
是不被识别为循环的 CFG 循环。 当然,其他
优化本质上更局部,并且在不可约的情况下工作得很好
配置文件。

根据记忆,可能是错误的,SPEC2006 在
401.bzip2 就是这样。 这在实践中相当罕见。

Clang 只会在使用的函数中发出一条间接指令指令
计算转到。 这具有将线程解释器变成
以indirectbr 块作为循环头的自然循环。 离开以后
LLVM IR,单个间接br 在代码生成器中尾部重复
重建原始的缠结。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983
或使线程静音
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

我还可以进一步说,如果将不可约构造添加到
WebAssembly,它们不能在 TurboFan(V8 的优化 JIT)中工作,所以这样
函数要么最终被解释(非常慢)要么被
由基线编译器编译(有点慢),因为我们可能不会
投入精力升级 TurboFan 以支持不可约控制流。
这意味着 WebAssembly 中具有不可约控制流的函数将
可能以更糟糕的表现告终。

当然,另一种选择是让 V8 中的 WebAssembly 引擎运行
relooper 来提供 TurboFan 可约图,但这会使编译
(和启动更糟)。 重新循环应该在我的
意见,否则我们将以不可避免的发动机成本告终。

最好的,
-本

2017 年 5 月 1 日星期一下午 12:48,Ben L. Titzer [email protected]写道:

不可约控制没有单通验证算法
我知道的流量。 仅可简化控制流的设计选择
受到这个要求的影响很大。

如前所述,不可约控制流至少可以建模两个
不同的方式。 带有 switch 语句的循环实际上可以优化
通过简单的局部跳转线程进入原始不可约图
优化(例如,通过折叠模式,其中分配一个常数
到局部变量,然后分支到条件分支
立即打开该局部变量)。

因此,不可约控制结构根本没有必要,它是
只需一个编译器后端转换即可恢复
原始不可约图并对其进行优化(对于其编译器的引擎
支持不可简化的控制流——这 4 个浏览器都不支持
据我所知)。

最好的,
-本

2017 年 4 月 20 日星期四上午 5:20,Jakob Stoklund Olesen <
通知@github.com> 写道:

LLVM 中的循环优化通常会忽略不可约控制流
而不是试图优化它。 他们基于的循环分析将
只识别自然循环,所以你只需要意识到可以
是不被识别为循环的 CFG 循环。 当然,其他
优化本质上更局部,并且在不可约的情况下工作得很好
配置文件。

根据记忆,可能是错误的,SPEC2006 有一个不可约循环
在 401.bzip2 中,就是这样。 这在实践中相当罕见。

Clang 只会在使用的函数中发出一条间接指令指令
计算转到。 这具有将线程解释器变成
以indirectbr 块作为循环头的自然循环。 离开以后
LLVM IR,单个间接br 在代码生成器中尾部重复
重建原始的缠结。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983
或使线程静音
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

已经建立了用于不可约控制流的线性时间验证的方法。 一个值得注意的例子是 JVM:使用堆栈映射,它具有线性时间验证。 WebAssembly 已经在每个类似块的构造上都有块签名。 在多个控制流路径合并的每个点都有明确的类型信息,因此没有必要使用定点算法。

(顺便说一句,不久前我问为什么不允许假设的pick运算符在任意深度读取其块之外的内容。这是一个答案:除非扩展签名以描述pick可能会读取,类型检查pick将需要更多信息。)

loop-with-a-switch 模式当然可以跳过线程,但依赖它是不切实际的。 如果引擎不对其进行优化,则会产生破坏性的开销。 如果大多数引擎确实对其进行了优化,那么通过将不可约控制流保持在语言本身之外的效果就不清楚了。

叹息……我本想早点回复,但生活挡住了路。

我一直在研究一些 JS 引擎,我想我必须削弱我对不可约控制流“正常工作”的说法。 我仍然不认为让它发挥作用有那么难,但是有些结构很难以一种实际上会受益的方式适应……

好吧,让我们假设,为了争论,使优化管道正确支持不可约控制流太难了。 JS 引擎仍然可以轻松地以一种 hacky 方式支持它,如下所示:

在后端,将labels块视为循环+切换,直到最后一分钟。 换句话说,当你看到一个labels块时,你把它当作一个循环头,外边指向每个标签,当你看到一个指向标签的branch ,你创建指向labels标头的边缘,而不是实际的目标标签 - 它应该单独存储在某个地方。 无需创建一个实际变量来存储目标标签,就像一个真正的循环+开关必须做的那样; 将值存储在分支指令的某些字段中就足够了,或者为此目的创建单独的控制指令就足够了。 然后,优化、调度,甚至寄存器分配都可以假装有两次跳转。 但是当需要实际生成本机跳转指令时,您检查该字段,并直接生成到目标标签的跳转。

例如,合并/删除分支的任何优化都可能存在问题,但应该很容易避免这种情况; 细节取决于发动机设计。

从某种意义上说,我的建议相当于@titzer 的“简单的本地跳转线程优化”。 我建议让“本机”不可约控制流看起来像一个循环+开关,但另一种方法是识别真正的循环+开关——也就是说,@titzer 的“将常量分配给局部变量的模式,然后一个分支到一个立即打开该局部变量的条件分支”——并添加元数据,允许在管道的后期删除间接分支。 如果这种优化变得无处不在,它可能是显式指令的一个不错的替代品。

无论哪种方式,hacky 方法的明显缺点是优化不理解真正的控制流图。 它们有效地表现得好像任何标签都可以跳转到任何其他标签。 特别是,寄存器分配必须将变量视为所有标签中的有效变量,即使它总是在跳转到特定标签之前被分配,如以下伪代码所示:

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

在某些情况下,这可能会导致严重的次优寄存器使用。 但正如我稍后会指出的,JIT 使用的活性算法可能根本无法做到这一点,无论如何……

无论如何,延迟优化总比不优化要好得多。 单次直接跳转比跳转+比较+加载+间接跳转好很多; CPU 分支预测器最终可能能够根据过去的状态预测后者的目标,但不如编译器可以。 您可以避免在“当前状态”变量上花费寄存器和/或内存。

至于表示,哪个更好:显式( labels指令或类似指令)或隐式(根据特定模式优化真实循环+切换)?

隐式的好处:

  • 保持规范精简。

  • 可能已经使用现有的循环+切换代码。 但是我还没有查看 binaryen 生成的东西,看看它是否遵循足够严格的模式。

  • 让表达不可约控制流的有福的方式感觉像是一种 hack,这突出了一个事实,即它通常比较慢,应该尽可能避免。

隐式的缺点:

  • 感觉就像一个黑客。 诚然,正如@titzer所说,它实际上并没有对“正确”支持不可约控制流的引擎造成不利影响。 他们可以及早识别模式并在执行优化之前恢复原始的不可约流。 尽管如此,只允许真正的跳跃似乎更整洁。

  • 创建一个“优化悬崖”,与 JS 相比,WebAssembly 通常应该避免这种情况。 回顾一下,要优化的基本模式是“将常量分配给局部变量,然后分支到立即打开该局部变量的条件分支”。 但是,如果,比如说,中间有一些其他指令,或者赋值实际上没有使用 wasm const指令,而仅仅是由于优化而被称为常量的东西,该怎么办? 一些引擎在他们认为的这种模式上可能比其他引擎更自由,但是利用这种模式的代码(有意或无意)将在浏览器之间具有截然不同的性能。 具有更明确的编码可以更清楚地设定期望。

  • 使得在假设的后处理步骤中像 IR 一样使用 wasm 变得更加困难。 如果一个以 wasm 为目标的编译器以正常方式做事,并在最终运行 relooper 并最终生成 wasm 之前使用内部 IR 处理所有优化/转换,那么它不会介意魔术指令序列的存在。 但是如果一个程序想要对 wasm 代码本身运行任何转换,它就必须避免破坏这些序列,这会很烦人。

无论如何,无论哪种方式,我都不在乎——只要我们决定隐式方法,主要浏览器实际上承诺执行相关的优化。

回到原生支持不可约流的问题——障碍是什么,有多少好处——这里有一些来自 IonMonkey 的优化通道的具体示例,必须对其进行修改以支持它:

AliasAnalysis.cpp:以反向后序(一次)迭代块,并通过仅查看以前看到的可能存在别名的存储来生成指令的排序依赖关系(如 InstructionReordering 中所用)。 这不适用于循环控制流。 但是(显式标记的)循环是经过特殊处理的,第二遍检查循环中的指令与同一循环中任何位置的任何后续存储。

-> 所以labels块必须有一些循环标记。 在这种情况下,我认为将整个labels块标记为循环将“正常工作”(无需专门标记各个标签),因为分析太不精确,无法关心循环内的控制流。

FlowAliasAnalysis.cpp:一种更智能的替代算法。 还以反向后序迭代块,但是在遇到每个块时,它会合并其每个前辈的计算的最后存储信息(假设已经被计算),除了循环头,它考虑了后边。

-> Messier 因为它假设 (a) 单个基本块的前身总是出现在它之前,除了循环后缘,并且 (b) 一个循环只能有一个后缘。 有不同的方法可以解决这个问题,但它可能需要显式处理labels ,并且为了使算法保持线性,在这种情况下它可能必须非常粗略地工作,更像是常规AliasAnalysis - 与 hacky 方法相比降低了收益。 不确定重量级编译器如何处理这种类型的优化。

BacktrackingAllocator.cpp:寄存器分配的类似行为:它通过指令列表进行线性反向传递,并假设指令的所有使用都将出现在其定义之后(即在之前处理),除非遇到循环后沿:寄存器是在循环的开头直播只需在整个循环中保持直播。

-> 每个标签都需要像循环头一样对待,但是活性必须扩展到整个标签块。 实施起来并不难,但同样,结果不会比 hacky 方法更好。 我认为。

@comex这里的另一个考虑因素是wasm 引擎应该做多少。 例如,您在上面提到了 Ion 的 AliasAnalysis,但另一方面,别名分析对于 WebAssembly 代码并不那么重要,至少目前大多数程序都使用线性内存。

Ion 的 BacktrackingAllocator.cpp 活跃度算法需要一些工作,但不会令人望而却步。 大多数 Ion 已经处理了各种形式的不可约控制流,因为 OSR 可以在循环中创建多个条目。

这里的一个更广泛的问题是 WebAssembly 引擎将进行哪些优化。 如果人们期望 WebAssembly 是一个类似程序集的平台,在生产者/库进行大部分优化的情况下具有可预测的性能,那么不可简化的控制流将是一个相当低的成本,因为引擎不需要大型复杂算法,因为它是一个巨大的负担. 如果人们期望 WebAssembly 是一种更高级别的字节码,它会自动进行更多高级优化,并且引擎更复杂,那么在语言之外保留不可简化的控制流就变得更有价值,以避免额外的复杂性。

BTW,本期还值得一提的是Braun等人的on-the-fly SSA构造算法,这是一种简单快速的on-the-fly SSA构造算法,支持不可约控制流。

我有兴趣在 iOS 上使用 WebAssembly 作为 qemu 后端,其中 WebKit(和动态链接器,但检查代码签名)是唯一允许将内存标记为可执行的程序。 Qemu 的 codegen 假设 goto 语句将成为它必须为其 codegen 的任何处理器的一部分,这使得 WebAssembly 后端几乎不可能在不添加 goto 的情况下进行。

@tbodt - 你能使用 Binaryen 的 relooper 吗? 这让您生成基本上是 Wasm-with-goto 的内容,然后将其转换为 Wasm 的结构化控制流。

@eholk这听起来比将机器代码直接翻译成wasm要慢得多。

@tbodt使用 Binaryen 确实会在途中添加一个额外的 IR,是的,但我认为它应该不会慢很多,它针对编译速度进行了优化。 除了处理 goto 等之外,它还可能有其他好处,因为您可以选择运行 Binaryen 优化器,这可能会做 qemu 优化器不做的事情(wasm 特定的事情)。

实际上,如果您愿意,我非常有兴趣与您合作 :) 我认为将 Qemu 移植到 wasm 会非常有用。

所以再想一想,gotos 并没有太大帮助。 Qemu 的 codegen 会在基本块第一次运行时生成代码。 如果一个块跳转到一个尚未生成的块,它会生成该块并使用 goto 将前一个块修补到下一个块。 据我所知,动态代码加载和现有函数的修补不是 webassembly 可以完成的事情。

@kripken我有兴趣合作,哪里是和你聊天的最佳地点?

您不能直接修补现有函数,但可以使用call_indirectWebAssembly.Table来 jit 代码。 对于任何尚未生成的基本块,您可以调用 JavaScript,同步生成 WebAssembly 模块和实例,提取导出的函数并将其写入表中的索引。 以后的调用将使用您生成的函数。

不过,我不确定是否有人尝试过这个,所以可能会有很多粗糙的边缘。

如果实施了尾声,那可能会奏效。 否则堆栈会很快溢出。

另一个挑战是在默认表中分配空间。 如何将地址映射到表索引?

另一种选择是在每个新的基本块上重新生成 wasm 函数。 这意味着重新编译的次数等于使用的块的数量,但我敢打赌这是让代码在编译后快速运行的唯一方法(尤其是内部循环),而且它不需要是完整的重新编译,我们可以为每个现有块重用 Binaryen IR,为新块添加 IR,然后在所有块上运行 relooper。

(但也许我们可以让 qemu 预先编译整个函数而不是懒惰地编译?)

@tbodt与 Binaryen 合作,一种选择是使用您的工作创建一个 repo(并且可以在那里使用问题等),另一种选择是在 Binaryen 中为 qemu 打开一个特定问题。

我们不能让 qemu 一次编译整个函数,因为 qemu 没有“函数”的概念。

至于重新编译整个块缓存,听起来可能需要很长时间。 我将弄清楚如何使用 qemu 的内置分析器,然后在 binaryen 上打开一个问题。

边问。 在我看来,以 WebAssembly 为目标的语言应该能够提供高效的相互递归功能。 为了描述它们的用处,我邀请您阅读: http :

特别是, Cheery 所表达的需求似乎是通过相互递归函数来解决的。

我了解尾递归的必要性,但我想知道是否只有在底层机器提供 goto 的情况下才能实现相互递归功能。 如果他们这样做了,那么对我来说,这是支持他们的合理论据,因为将会有大量的编程语言很难以其他方式定位 WebAssembly。 如果他们不这样做,那么支持相互递归函数的最小机制可能就是所需要的(以及尾递归)。

@davidgrenier ,Wasm 模块中的函数都是相互递归的。 你能详细说明你认为它们效率低下的地方吗? 您是指没有尾音还是其他什么?

一般尾声即将到来。 尾递归(相互或其他)将是一个特例。

我并不是说他们有什么效率低下的地方。 我是说,如果你有它们,你就不需要通用 goto,因为相互递归的函数提供了针对 WebAssembly 的语言实现者应该需要的所有东西。

Goto 对于从可视化编程中的图表生成代码非常有用。 也许现在可视化编程不是很流行,但未来它可以吸引更多人,我认为 wasm 应该做好准备。 更多关于从图表和转到代码生成: http :

即将发布的 Go 1.11 版本将对 WebAssembly 提供实验性支持。 这将包括对 Go 的所有功能的完全支持,包括 goroutines、channels 等。但是,生成的 WebAssembly 的性能目前并不是那么好。

这主要是因为缺少 goto 指令。 如果没有 goto 指令,我们不得不在每个函数中使用顶层循环和跳转表。 使用 relooper 算法对我们来说不是一个选项,因为在 goroutine 之间切换时,我们需要能够在函数的不同点恢复执行。 relooper 对此无能为力,只有 goto 指令可以。

WebAssembly 能够支持像 Go 这样的语言真是太棒了。 但要成为真正的 Web 汇编,WebAssembly 应该与其他汇编语言一样强大。 Go 有一个高级编译器,它能够为许多其他平台发出非常高效的程序集。 这就是为什么我想争辩说,它主要是 WebAssembly 的限制,而不是 Go 编译器的限制,它不可能也使用这个编译器来为 web 发出有效的汇编。

使用 relooper 算法对我们来说不是一个选项,因为在 goroutine 之间切换时,我们需要能够在函数的不同点恢复执行。

只是为了澄清一下,常规的 goto 是不够的,您的用例需要计算的 goto ,对吗?

我认为就性能而言,常规 goto 可能就足够了。 基本块之间的跳转无论如何都是静态的,并且对于切换 goroutines,在其分支中带有 gotos 的br_table应该足够高效。 输出大小是一个不同的问题。

听起来您在每个函数中都有正常的控制流,但在恢复 goroutine 时,还需要能够从函数入口跳转到“中间”的某些其他位置 - 有多少这样的位置? 如果它是每一个基本块,那么 relooper 将被迫发出每条指令都经过的顶层循环,但如果它只是几个,那应该不是问题。 (这实际上是 emscripten 中的 setjmp 支持所发生的 - 我们只是在 LLVM 的基本块之间创建额外的必要路径,并让 relooper 正常处理。)

对某个其他函数的每次调用都是这样一个位置,并且大多数基本块至少有一个调用指令。 我们或多或少地展开和恢复调用堆栈。

我明白了,谢谢。 是的,我同意要实现这一点,您需要静态 goto 或调用堆栈恢复支持(也已考虑过)。

是否可以在 CPS 样式中调用函数或在 WASM 中实现call/cc

@Heimdell ,路线图上支持某种形式的定界延续(又名“堆栈切换”),这对于几乎任何有趣的控制抽象来说应该足够了。 但是,我们不能支持未定界的延续(即完整的调用/cc),因为 Wasm 调用堆栈可以与其他语言任意混合,包括对嵌入器的可重入调用,因此不能假定是可复制或可移动的。

通读这个帖子,我觉得任意标签和 goto 在成为功能之前有一个主要障碍:

  • 非结构化控制流使不可约控制流图成为可能
  • 消除*任何“快速、简单的验证、轻松、一次性转换为 SSA 表格”
  • 开放 JIT 编译器以实现非线性性能
  • 如果原始语言编译器可以完成前期工作,那么浏览网页的人就不应该遭受延迟

_*尽管可能有替代方案,例如Braun 等人的处理不可约控制流的动态

如果我们仍然卡在那里,_and_ 尾调用正在向前发展,也许值得要求语言编译器仍然翻译为 goto,但作为 WebAssembly 输出之前的最后一步,将“标签块”拆分为函数,并且将 goto 转换为尾调用。

根据 Scheme 设计师 Guy Steele 1977 年的论文Lambda: The Ultimate GOTO ,转换应该是可能的,并且尾调用的性能应该能够与 goto 紧密匹配。

想法?

如果我们仍然卡在那里,_and_ 尾调用正在向前发展,也许值得要求语言编译器仍然翻译为 goto,但作为 WebAssembly 输出之前的最后一步,将“标签块”拆分为函数,并且将 goto 转换为尾调用。

这基本上是每个编译器都会做的事情,据我所知,没有人提倡在 JVM 中导致如此多问题的非托管 goto,只是为了一个类型化的 EBB 图表。 LLVM、GCC、Cranelift 和其余的都有一个(可能是不可约化的)SSA 形式的 CFG 作为它们的内部表示,并且从 Wasm 到 Native 的编译器具有相同的内部表示,所以我们希望尽可能多地保留这些信息,并且尽可能少地重建这些信息。 Locals 是有损的,因为它们不再是 SSA,而 Wasm 的控制流是有损的,因为它不再是任意的 CFG。 具有 Wasm 的 AFAIK 是具有嵌入式细粒度寄存器活性信息的无限寄存器 SSA 寄存器机器可能是代码生成的最佳选择,但代码大小会膨胀,具有在任意 CFG 上建模的控制流的堆栈机器可能是最好的中间立场. 不过,我可能对注册机的代码大小有误,但可以有效地对其进行编码。

关于不可约控制流的问题是,如果它在前端是不可约的,它在 wasm 中仍然是不可约的,relooper/stackifier 转换不会使控制流可约,它只是将不可约性转换为依赖于运行时值。 这为后端提供了更少的信息,因此它可以生成更糟糕的代码,目前为不可约 CFG 生成良好代码的唯一方法是检测 relooper 和 stackifier 发出的模式并将它们转换回不可约 CFG。 除非您正在开发 V8,而 AFAIK 仅支持可简化的控制流,否则支持不可简化的控制流纯粹是一种胜利——它使前端和后端都变得更简单(前端可以以它们内部存储的相同格式发出代码,后端不'不必检测模式),同时在控制流不可约的情况下产生更好的输出,并且在控制流可约简的通常情况下输出同样好或更好。

此外,它将允许 GCC 和 Go 开始生产 WebAssembly。

我知道 V8 是 WebAssembly 生态系统的重要组成部分,但它似乎是该生态系统中唯一受益于当前控制流情况的部分,我知道的所有其他后端无论如何都转换为 CFG 并且不受WebAssembly 是否可以表示不可约控制流。

v8 不能仅仅合并 relooper 以接受输入 CFG 吗? 似乎生态系统的大部分都被 v8 的实现细节所阻止。

仅供参考,我注意到 c++ 中的 switch 语句在 wasm 中非常慢。 当我分析代码时,我必须将它们转换为其他形式,这些形式可以更快地进行图像处理。 这在其他架构上从来都不是问题。 出于性能原因,我真的很想去。

@graph ,您能否提供有关“switch 语句如何缓慢”的更多详细信息? 一直在寻找提高性能的机会...(如果您不想陷入此线程,请直接给我发电子邮件,[email protected]。)

我会在这里发布,因为这适用于所有浏览器。 像这样的简单语句在使用 emscripten 编译时转换为 if 语句时速度更快。

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

我假设编译器正在将跳转表转换为 wasm 支持的任何内容。 我没有查看生成的程序集,所以无法确认。

我知道一些与 wasm 无关的东西可以针对网络上的图像处理进行优化。 我已经通过 Firefox 中的“反馈”按钮提交了它。 如果您有兴趣,请告诉我,我会通过电子邮件将问题发送给您。

@graph一个完整的基准测试在这里会很有帮助。 一般来说,C 中的一个 switch 可以在 wasm 中变成一个非常快速的跳转表,但是有些极端情况还不能很好地工作,我们可能需要在 LLVM 或浏览器中修复。

特别是在 emscripten 中,旧的 fastcomp 后端和新的上游后端之间切换的处理方式发生了很大变化,所以如果您在不久前或最近看到这一点,但使用的是 fastcomp,最好检查一下上游。

@graph ,如果 emscripten 生成一个 br_table ,那么 jit 有时会生成一个跳转表,有时(如果它认为它会更快)它会线性搜索键空间或使用内联二进制搜索。 它的作用通常取决于开关的大小。 当然,选择策略可能不是最优的......我同意@kripken ,如果你有一些可运行的代码在这里会非常有帮助。

(不了解 v8 或 jsc,但 Firefox 目前不将 if-then-else 链识别为可能的开关,因此打开代码开关通常不是一个好主意,只要 if-then-else 链。收支平衡点可能不超过两三个比较。)

@lars-t-hansen @kripken @graph很可能br_table目前只是非常未优化,因为此交换似乎表明: https :

@aardappel ,这很好奇,我昨天运行的基准测试没有显示这一点,在我系统上的 Firefox 中,我记得的收支平衡点大约是 5 个案例,之后 br_table 是赢家。 当然是微基准测试,并尝试均匀分布查找键。 如果“if”嵌套偏向于最可能的键,因此只需要几个测试,那么“if”嵌套将获胜。

如果它不能对开关值进行范围分析来避免它,那么 br_table 还必须对开关的范围进行至少一次过滤测试,这也占用了它的优势。

@lars-t-hansen 是的,我们不知道他的测试用例,可能它有异常值。 无论哪种方式,看起来 Chrome 都比 Firefox 有更多的工作要做。

我在度假,因此我没有回复。 感谢您的理解。

@kripken @lars-t-hansen 我已经进行了一些测试,似乎是的,现在在 Firefox 中是更好的。 在某些情况下,if-else 的性能优于 switch。 这是一个案例:


主文件

#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;
}

取决于 switchSelect 的值。 if-else 表现优异。 示例输出:

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

如您所见, switchSelect = 32 if-else 更快。 对于其他情况,if-else 会更快一些。 对于 switchSelect = 1 & 0 的情况,switch 语句更快。

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)

使用截至 2019 年 10 月 20 日的最新稳定版 emscripen。全新安装./emcc activate latest

我注意到上面有一个错字,但它不应该影响 if-else 更快的 SW3 情况,因为它们正在执行相同的指令。

再次超过盈亏平衡点5:有趣的是,对于这种情况,switchSelect = 32的速度与if-else相似。 如您所见,1003 if-else 稍快一些。 在这种情况下,Switch 应该会赢。

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


主文件

#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;
}


感谢你们查看这些测试用例。

虽然这是一个非常稀疏的switch ,但 LLVM 无论如何都应该将其转换为等效于一组 if-then 的方法,但显然它的效率低于手动 if-thens。 您是否尝试过运行 wasm2wat 来查看这两个循环的代码有何不同?

这也很大程度上取决于在每次迭代中使用相同值的测试。 如果它循环遍历所有值,或者更好的是,从它们中随机选择(如果可以便宜地完成),这个测试会更好。

更好的是,人们使用 switch 来提高性能的真正原因是范围很广,所以你可以保证它实际上是在下面使用br_table 。 看看有多少案例br_tableif更快将是最有用的知识。

使用紧密循环中的开关是因为它是更简洁的代码而不是性能。 但是对于 wasm,性能影响太大,所以它被转换为更丑的 if 语句。 对于我的许多用例中的图像处理,如果我想从开关中获得更高的性能,我会将开关移到循环之外,并为每种情况简单地复制循环。 通常,切换只是在某种形式的像素格式、颜色格式、编码等之间切换……在许多情况下,常量是通过定义或枚举计算的,而不是线性的。 我现在看到我的问题与 goto 设计无关。 我只是对我的 switch 语句发生的事情有一个不完整的理解。 我希望我的笔记对阅读这篇文章的浏览器开发人员有用,以便在这些情况下优化 wasm 以进行图像处理。 谢谢你。

我从没想过 goto 会引起如此激烈的争论😮。 我在每种语言的船上都应该有一个 goto 😁。 添加 goto 的另一个原因是它降低了编译器编译为 wasm 的复杂性。 我很确定上面某处提到过。 现在我没有什么可抱怨的了😞。

那里有进一步的进展吗?

由于激烈的辩论,我认为某些浏览器会添加对 goto 的支持作为非标准字节码扩展。 那么也许GCC可以作为支持非标准版本进入游戏。 我认为总体上不是很好,但会允许更多的编译器竞争。 考虑过这个吗?

最近没有太大进展,但你可能想看看funclets 提案

@graph对我来说,你的建议听起来像“让我们打破一切,希望更好”。
它不是那样工作的。 当前的 WebAssembly 结构有很多好处(不幸的是,这些好处并不明显)。 尝试更深入地了解 wasm 的哲学。

允许“任意标签和 Gotos”将使我们回到不可验证字节码的(古代)时代。 所有的编译器都会切换到一种“懒惰的方式”做事,而不是“做对了”。

很明显,wasm 目前的状态有一些重大遗漏。 人们正在努力填补国内空白(如由@binji提到的),但我认为将被修补“全球WASM结构”的需要。 只是我的拙见。

@vshymanskyy funclets提案提供了与任意标签和

我还应该提到,在我们的线性时间 Wasm 编译器中,我们在内部将所有 Wasm 控制流编译成类似 funclets 的表示,我在这篇文章中提供了一些信息在这里。 编译器从这种类似 funclets 的表示中获取其所有类型信息,因此可以说在线性时间内验证它的类型安全性是微不足道的。

我认为这种无法在线性时间内验证不可约控制流的误解源于 JVM,其中不可约控制流必须使用解释器而不是编译来执行。 这是因为 JVM 没有任何方法来表示不可约控制流的类型元数据,因此它无法进行从堆栈机到注册机的转换。 “任意 goto”(即跳转到字节/指令 X)根本无法验证,但是将函数分成类型块,然后可以以任意顺序在它们之间跳转,并不比将模块分成类型函数更难验证,然后可以以任意顺序在它们之间跳转。 您不需要跳转到字节 X 样式的无类型 goto 来实现任何有用的模式,这些模式将由 GCC 和 LLVM 等编译器发出。

我只是喜欢这里的过程。 A 面解释了为什么在特定应用中需要这样做。 B 方说他们做错了,但不支持该应用程序。 A 面解释了 B 的务实论点是如何站不住脚的。 B方不想处理它,因为他们认为A方做错了。 A方正在努力实现一个目标。 B 方说这是错误的目标,称其为懒惰或野蛮。 A 方面失去了更深层次的哲学意义。 B 方面失去了实用主义,因为他们声称拥有某种更高的道德基础。 A 方认为这是一种不道德的机械操作。 最终,无论好坏,B 方通常都会控制规范,并且他们已经完成了令人难以置信的数量与他们的相对纯度。

老实说,我只是把鼻子插在这里,因为几年前,我试图制作一个到 WASM 的 TinyCC 端口,这样我就可以在针对 ESP8266 的 ESP8266 上运行开发环境。 我只有大约 4MB 的存储空间,所以包括重新循环和切换到 AST 以及许多其他更改都是不可能的。 (旁注:relooper 是如何唯一像 relooper 的?太可怕了,没有人用 C 重写那个傻瓜!?)即使在这一点上可能,我不知道我是否会写一个 TinyCC 目标对 WASM 来说,因为它对我来说不再那么有趣了。

不过,这个线程。 天哪,这条线给我带来了如此多的存在主义快乐。 观察人类的分歧比民主、共和或宗教更深。 我觉得这是否可以解决。 如果 A 可以生活在 B 的世界中,或者 B 验证 A 的说法,即程序编程有它的位置......我觉得我们可以解决世界和平。

负责 V8 的人能否在此线程中确认反对不可约控制流不受 V8 当前实现的影响?

我问是因为这是最让我烦恼的。 对我来说,这似乎应该是关于这个功能的优缺点的规范级别的讨论。 它根本不应该受到当前特定实现的设计方式的影响。 然而,有一些声明让我相信 V8 的实现正在影响这一点。 也许我错了。 公开声明可能会有所帮助。

好吧,尽管不幸的是,目前存在的当前实现是如此重要,以至于未来(可能比过去更长)并不那么重要。 我试图在#1202 中解释一致性比少数实现更重要,但我似乎在妄想。 祝你好运,说明某些项目中某处的某些开发决策并不构成普遍真理,或者默认情况下必须被假定为正确。

该线程是 W3C 煤矿中的一只金丝雀。 尽管我非常尊重许多 W3C 人员,但将 JavaScript 委托给 Ecma International 而不是 W3C 的决定并非没有偏见。

@cnlohr一样,我对 TCC wasm 端口抱有希望,这是有充分理由的;

“Wasm 被设计为编程语言的可移植编译目标,支持在 Web 上部署客户端和服务器应用程序。” -webassembly.org

当然,任何人都可以断言为什么goto是 [INSERT JARGON],但是我们更喜欢标准而不是意见怎么样。 我们都同意 POSIX C 是一个很好的基准目标,特别是考虑到今天的语言要么是由 C 编写的,要么是基于 C 的,并且 WASM 的主页标题吹嘘自己是一个可移植的语言编译目标。 当然,某些功能将被规划,例如线程和 simd。 但是,完全无视像goto这样基本的东西,甚至不给它提供路线图的体面,这与 WASM 的既定目的以及标准化机构的这种立场不一致,即为<marquee>开绿灯超出了苍白。

根据SEI CERT C 编码标准建议。 “在使用和释放资源时让函数出错时考虑使用 goto 链”

许多功能需要分配多种资源。 如果在此函数中间的某个位置失败并返回而不释放所有已分配的资源,可能会产生内存泄漏。 忘记以这种方式释放一个(或全部)资源是一个常见的错误,因此 goto 链是组织出口的最简单和最干净的方式,同时保持释放资源的顺序。

然后,该建议提供了一个使用goto的首选 POSIX C 解决方案示例。 反对者会指出goto仍然被认为是有害的。 有趣的是,这种观点并没有体现在这些特定的编码标准中,只是一个注释。 这将我们带到了“被认为有害”的金丝雀。

底线,考虑“CSS 区域”或goto是有害的,只应与使用此类功能的问题的建议解决方案一起权衡。 如果删除所说的“有害”功能等于删除了没有其他选择的合理用例,那不是解决方案,这实际上对语言的用户有害。

即使在 C 语言中,函数也不是零成本。如果有人提供 gotos 和标签的替代品,请 Canihaz! 如果有人说我不需要它,他们怎么知道? 在性能方面, goto可以给我们一点额外的东西,很难向工程师争论我们不需要自该语言诞生以来就存在的高性能、易于理解的特性。

没有计划支持goto ,WASM 是一个玩具编译目标,没关系,也许这就是 W3C 看待网络的方式。 我希望 WASM 作为标准能够达到更高的标准,超越 32 位地址空间,并进入编译竞赛。 我希望工程话语可以摆脱“那不可能......”让我们快速跟踪 GCC C 扩展,如标签作为值,因为 WASM 应该很棒。 就个人而言,TCC 在这一点上更令人印象深刻且更有用,没有所有浪费的夸夸其谈,没有时髦的登陆页面和闪亮的标志。

@d4tocchini

根据SEI CERT C 编码标准建议。 “在使用和释放资源时让函数出错时考虑使用 goto 链”

许多功能需要分配多种资源。 如果在此函数中间的某个位置失败并返回而不释放所有已分配的资源,可能会产生内存泄漏。 忘记以这种方式释放一个(或全部)资源是一个常见的错误,因此 goto 链是组织出口的最简单和最干净的方式,同时保持释放资源的顺序。

然后,该建议提供了一个使用goto的首选 POSIX C 解决方案示例。 反对者会指出goto仍然被认为是有害的。 有趣的是,这种观点并没有体现在那些特定的编码标准中,只是一个注释。 这将我们带到了“被认为有害”的金丝雀。

该推荐中给出的示例可以直接用标记的中断表示,这些中断在 Wasm 中可用。 它不需要任意 goto 的额外功能。 (C 不提供标记的 break 和 continue,因此必须经常退回到 goto。)

@rossberg ,在该示例中标记中断的观点很好,但我不同意您的定性假设,即 C 必须“回退”。 goto 是一个比标记中断更丰富的构造。 如果要将 C 包含在可移植编译目标中,并且 C 不支持带标签的中断,那将是一个静音点。 Java 标记了中断/继续,而 Python拒绝了提议的功能,并且考虑到 sun JVM 和默认的 CPython 都是用 C 编写的,您不同意 C 作为受支持的语言应该在优先级列表中更高吗?

如果goto很容易被忽略,是否应该重新考虑数百次使用

有没有不能用 C 编写的语言? C 作为一种语言应该通知 WASM 的特性。 如果当今的 WASM 无法使用 POSIX C,那么就有了正确的路线图。

不是真正关于论点的主题,而是不要掩盖随机错误在一般论点中到处潜伏的阴影:

Python 已标记中断

你能详细说明吗? (又名:Python 没有标记的中断。)

@pfalcon ,是的,我的错,我编辑了我的评论以澄清 python提出的标记中断/继续并拒绝它

如果 goto 如此容易被忽略,是否应该重新考虑在 emscripten 的源代码中对 goto 的数百次使用?

1) 请注意其中有多少存在于 musl libc 中,而不是直接存在于 emscripten 中。 (第二个最常用的是测试/第三方)
2) 源级构造与字节码指令不同
3) Emscripten 与 wasm 标准的抽象级别不同,因此,不应该在此基础上重新考虑。

具体来说,今天从 libc 中重写 goto 可能很有用,因为这样我们就可以更好地控制生成的 cfg,而不是信任 relooper/cfgstackify 来处理它。 我们没有这样做,因为要从上游 musl 中获得截然不同的代码,这是一项非常重要的工作。

Emscripten 开发人员(我最后一次检查)倾向于认为类似 goto 的结构会非常好,出于这些显而易见的原因,因此不太可能将其从考虑中删除,即使需要数年时间才能达到可接受的妥协。

标准化机构的这种立场,即绿灯<marquee>超出了苍白。

这是一个特别愚蠢的陈述。

1) We-the-broader-Internet 距离做出这个决定还有十多年的时间
2) We-the-wasm-CG 是完全(几乎?)与该标签不同的一群人,并且可能个人也对明显的过去错误感到恼火。

没有所有浪费的夸夸其谈,没有时髦的登陆页面和闪亮的标志。

这可以改写为“我很沮丧”而不会遇到语气问题。

正如这个线程所示,这些对话已经够难了。

当您想要为所有新功能重写一个深受信任和理解的功能集时,您会感到非常担忧,因为它们的使用环境必须经过额外的步骤来支持它。 (尽管我仍然坚定地支持 please-add-goto 阵营,因为我讨厌只使用一个特定的编译器)

我认为这个线程已经超越了生产力——它已经运行了四年多,看起来这里使用了支持和反对任意goto的所有可能的论点; 还应该注意的是,这些论点都不是特别新的;)

有些托管运行时选择不使用任意跳转标签,这对他们来说效果很好。 此外,还有允许任意跳转的编程系统,它们也做得很好。 最后,编程系统的作者做出设计选择,只有时间才能真正证明这些选择是否成功。

禁止任意跳转的 Wasm 设计选择是其理念的核心。 没有类似 funclets 的东西,它不太可能支持goto s,原因与它不支持纯间接跳转的原因相同。

禁止任意跳转的 Wasm 设计选择是其理念的核心。 没有类似 funclet 的东西,它不太可能支持 goto,原因与它不支持纯间接跳转的原因相同。

@penzn为什么提案被卡住了? 它自 2018 年 10 月以来就存在,目前仍处于第 0 阶段。

如果我们正在讨论一个普通的开源项目,我会分叉并完成它。 我们在这里谈论的是影响深远的垄断标准。 因为我们关心,所以应该培养积极的社区反应。

@J0eCool

  1. 请注意其中有多少存在于 musl libc 中,而不是直接存在于 emscripten 中。 (第二个最常用的是测试/第三方)

是的,点头是它在 C 中的使用量。

  1. 源级结构与字节码指令不同

当然,我们讨论的是影响源代码级别构造的内部问题。 这是沮丧的一部分,黑匣子不应该泄露它的担忧。

  1. Emscripten 与 wasm 标准的抽象级别不同,因此,不应该在此基础上重新考虑。

关键是你会在大多数大型 C 项目中找到goto ,即使在整个 WebAssembly 工具链中也是如此。 一般语言的可移植编译器目标没有足够的表达力以针对其自己的编译器,这与我们企业的性质并不完全一致。

具体来说,今天从 libc 中重写 goto 可能很有用,因为这样我们就可以更好地控制生成的 cfg,而不是信任 relooper/cfgstackify 来处理它。

这是圆形的。 上述许多人提出了关于这种要求的无误性的严重未回答的问题。

我们没有这样做,因为要从上游 musl 中获得截然不同的代码,这是一项非常重要的工作。

正如您所说,可以删除 goto,这是一项非常重要的工作! 您是否建议其他人因为不应该支持 goto 而分散代码路径?

Emscripten 开发人员(我最后一次检查)倾向于认为类似 goto 的结构会非常好,出于这些显而易见的原因,因此不太可能将其从考虑中删除,即使需要数年时间才能达到可接受的妥协。

一线希望! 如果 goto/label 支持被认真对待,路线图项目 + 正式邀请让球动起来,我会很满意,即使是几年后。

这是一个特别愚蠢的陈述。

你说得对。 原谅我的夸张,我有点沮丧。 我喜欢 wasm,并且经常使用它,但如果我想用它做任何值得注意的事情,比如 TCC 端口,我最终会看到一条痛苦的道路。 在阅读了所有评论和文章后,我仍然无法确定反对意见是技术性的、哲学性的还是政治性的。 正如@neelance 所说

“V8 的负责人能否在此线程中确认反对不可约控制流不受 V8 当前实现的影响?

我问是因为这是最让我烦恼的。 [...]

如果你们听到任何使用,请记住 @neelance 关于 Go 1.11 的反馈。 这很难反驳。 当然,我们都可以对 goto 进行非平凡的除尘,但即便如此,我们也会遭受严重的性能打击,只能通过 goto 指令来修复。

再次,请原谅我的沮丧,但如果这个问题在没有适当地址的情况下关闭,那么我担心它会发出错误的信号,只会激怒这些社区的反应,并且不适合我们最伟大的标准努力之一场地。 不用说,我是这支球队所有人的忠实粉丝和支持者。 谢谢!

这是由缺少 goto/funclets 引起的另一个现实世界问题: https :

对于这个程序,Go 编译器当前生成一个带有 18,000 个嵌套block的 wasm 二进制文件。 wasm 二进制文件本身的大小为 2.7MB,但是当我通过wasm2wat运行它时,我得到一个 4.7GB 的 .wat 文件。 🤯

我可以尝试给 Go 编译器一些启发式方法,这样它就可以创建某种二叉树,而不是一个巨大的跳转表,然后多次查看跳转目标变量。 但这真的是 wasm 应该的样子吗?

我想补充一点,我觉得奇怪的是,人们似乎认为如果只有一个编译器 (Emscripten[1]) 可以实际支持 WebAssembly 就很好。
让我想起了 libopus 的情况(一个通常依赖于受版权保护的代码的标准)。

我也觉得奇怪的是,WebAssembly 开发人员似乎如此强烈地反对这一点,尽管编译器端的几乎每个人都告诉他们这是必需的。 请记住:WebAssembly 是一种标准,而不是宣言。 事实上,大多数现代编译器在内部使用某种形式的 SSA + 基本块(或具有相同属性的几乎等效的东西),它们没有显式循环的概念[2]。 甚至 JIT 也使用类似的东西,这就是它的普遍性。
据我所知[3],在没有“只使用 goto”的情况下进行重新循环的绝对要求是在语言到语言的翻译之外是前所未有的 --- 即便如此,只有语言到语言的翻译才能做到这一点定位无 goto 语言。 特别是,除了 WebAssembly 之外,我从未听说过必须对任何类型的 IR 或字节码执行此操作。

也许是时候将 WebAssembly 重命名为 WebEmscripten(WebScripten?)了。

正如@d4tocchini所说,如果不是因为 WebAssembly(由于标准化情况是必要的)垄断地位,它现在可能已经被分叉成可以合理支持编译器开发人员已经知道它需要支持的东西。
不,“只使用 emscripten”不是一个有效的反驳论点,因为它使标准依赖于单个编译器供应商。 我希望我不需要告诉你为什么这很糟糕。

编辑:我忘了添加一件事:
你还没有澄清这个问题是技术性的、哲学性的还是政治性的。 我怀疑后者,但很高兴被证明是错误的(因为技术和哲学问题比政治问题更容易解决)。

这是由于缺少 goto/ funclets引起的另一个现实世界问题:

对于这个程序,Go 编译器当前生成一个带有 18,000 个嵌套block的 wasm 二进制文件。 wasm 二进制文件本身的大小为 2.7MB,但是当我通过wasm2wat运行它时,我得到一个 4.7GB 的 .wat 文件。 🤯

我可以尝试给 Go 编译器一些启发式方法,这样它就可以创建某种二叉树,而不是一个巨大的跳转表,然后多次查看跳转目标变量。 但这真的是 wasm 应该的样子吗?

这个例子真的很有趣。 这么简单的直线程序是如何生成这段代码的呢? 数组元素个数和块数有什么关系? 特别是,我是否应该将其解释为每个数组元素访问都需要忠实地编译_multiple_块?

不,“只使用 emscripten”不是一个有效的反驳论点

我认为在这种情况下真正的反对论点是另一个想要针对 Wasm 的编译器可以/必须实现他们自己的类似 relooper 的算法。 就个人而言,我确实认为 Wasm 最终应该有一个多体循环(接近 funclets)或类似的东西,这是goto的自然目标。

@conrad-watt 有几个因素导致每个分配使用 CFG 中的几个基本块。 其中之一是对切片进行长度检查,因为在编译时长度是未知的。 一般来说,我会说编译器认为基本块是一种相对便宜的结构,但使用 wasm 时它们有点贵,尤其是在这种特殊情况下。

@neelance在修改后的示例中,代码在几个函数之间拆分,(运行时/编译)内存开销显示要低得多。 在这种情况下生成的块更少,或者仅仅是单独的函数意味着引擎 GC 可以更细粒度?

@conrad-watt 甚至不是使用内存的 Go 代码,而是 WebAssembly 主机:当我使用 Chrome 86 实例化 wasm 二进制文件时,我的 CPU 达到 100% 持续 2 分钟,并且选项卡的内存使用量达到峰值11.3 GB。 这是执行 wasm 二进制 / Go 代码

这已经是我的理解了。 我希望大量的块/类型注释会导致内存开销,特别是在编译/实例化期间。

尝试消除我之前的问题 - 如果代码的拆分版本以更少的块编译为 Wasm(由于一些 relooper 怪癖),这将是减少内存开销的一种解释,并且将是添加更多通用的一个很好的动机控制流到 Wasm。

或者,可能是拆分代码导致(大致)相同的总块数,但因为每个函数都是单独 JIT 编译的,所以用于编译每个函数的元数据/IR 可以更热切地被 Wasm 引擎 GC'd . 多年前的 V8 在解析/编译大型 asm.js 函数时也出现了类似的问题。 在这种情况下,向 Wasm 引入更通用的控制流并不能解决问题。

首先我想澄清一下:Go 编译器没有使用 relooper 算法,因为它本质上与切换 goroutine 的概念不兼容。 所有基本块都通过一个跳转表表示,并在可能的情况下进行一些下降。

我猜 Chrome 的 wasm 运行时在嵌套block s 的深度方面存在一些指数复杂性增长。 拆分版本具有相同数量的块,但最大深度更小。

在这种情况下,向 Wasm 引入更通用的控制流并不能解决问题。

我同意这个复杂性问题可能会在 Chrome 端得到解决。 但我总是喜欢问“为什么这个问题首先存在?”的问题。 我会争辩说,如果使用更一般的控制流程,这个问题将永远不会存在。 此外,由于所有基本块都表示为跳转表,因此仍然存在显着的一般性能开销,我认为这不太可能通过优化消失。

我想 Chrome 的 wasm 运行时在嵌套块的深度方面有一些指数级的复杂性增长。 拆分版本具有相同数量的块,但最大深度更小。

这是否意味着在具有 N 个数组访问的直线函数中,最终的数组访问将嵌套(某个常数因子)N 个块深度? 如果是这样,有没有办法通过不同地分解错误处理代码来减少这种情况? 我希望任何编译器都必须分析 3000 个嵌套循环(非常粗略的类比),所以如果由于语义原因这是不可避免的,那么这也将成为更通用控制流的一个论据。

如果嵌套差异没有那么明显,我的预感是 V8 几乎不会在元数据 _during_ 编译单个 Wasm 函数时进行 GC'ing,所以即使我们从一开始就有类似调整过的 funclets 提案的语言,如果没有他们做一些有趣的 GC 优化,同样的开销仍然是可见的。

此外,由于所有基本块都表示为跳转表,因此仍然存在显着的一般性能开销,我认为这不太可能通过优化消失。

同意在这里有一个更自然的目标显然更可取(从纯粹的技术角度来看)。

这是否意味着在具有 N 个数组访问的直线函数中,最终的数组访问将嵌套(某个常数因子)N 个块深度? 如果是这样,有没有办法通过不同地分解错误处理代码来减少这种情况? 我希望任何编译器都必须分析 3000 个嵌套循环(非常粗略的类比),所以如果由于语义原因这是不可避免的,那么这也将成为更通用控制流的一个论据。

反过来:第一个作业嵌套得那么深,而不是最后一个。 嵌套的block和顶部的单个br_table是传统switch语句在 wasm 中的表达方式。 这是我提到的跳表。 没有 3000 个嵌套循环。

如果嵌套差异没有那么明显,我的预感是 V8 在编译单个 Wasm 函数期间几乎不会对元数据进行 GC,所以即使我们从一开始就有类似调整过的 funclets 提案的语言,如果没有他们做一些有趣的 GC 优化,同样的开销仍然是可见的。

是的,可能还有一些实现在基本块的数量方面具有指数复杂性。 但是处理基本块(即使是大量的)是很多编译器整天都在做的事情。 例如,Go 编译器本身在编译期间很容易处理这么多的基本块,即使它们经过多次优化处理。

是的,可能还有一些实现在基本块的数量方面具有指数复杂性。 但是处理基本块(即使是大量的)是很多编译器整天都在做的事情。 例如,Go 编译器本身在编译期间很容易处理这么多的基本块,即使它们经过多次优化处理。

当然,但这里的性能问题与这些基本块之间的控制流在原始源语言中的表达方式是正交的(即不是 Wasm 中更通用控制流的动机)。 要查看 V8 在这里是否特别糟糕,可以检查 FireFox/SpiderMonkey 或 Lucet/Cranelift 是否表现出相同的编译开销。

我做了更多的测试:Firefox 和 Safari 完全没有问题。 有趣的是,Chrome 甚至能够在密集的过程完成之前运行代码,因此运行 wasm 二进制文件并非绝对必要的一些任务似乎存在复杂性问题。

当然,但是这里的性能问题与这些基本块之间的控制流在原始源语言中的表达方式是正交的。

我明白你的意思了。

我仍然相信,不是通过跳转指令而是通过跳转变量和巨大的跳转表/嵌套块来表示基本块正在以相当复杂的方式表达基本块的简单概念。 这会导致性能开销和复杂性问题的风险,例如我们在此处看到的问题。 我相信更简单的系统比复杂系统更好、更健壮。 我还没有看到让我相信更简单的系统是一个糟糕选择的论点。 我只听说 V8 很难实现任意控制流,我的未解决问题告诉我这个说法是错误的(https://github.com/WebAssembly/design/issues/796#issuecomment-623431527)并没有还没有回答。

@neelance

Chrome 甚至可以在密集的过程完成之前运行代码

听起来基线编译器 Liftoff 还可以,问题出在优化编译器 TurboFan 上。 请提交一个问题,或者请提供一个测试用例,如果你愿意,我可以提交一个。

更笼统地说:你认为wasm 堆栈切换计划能够解决 Go 的 goroutine 实现问题吗? 这是我能找到的最好的链接,但它现在非常活跃,每两周举行一次会议,以及几个激励工作的强大用例。 如果 Go 可以使用 wasm 协程来避免大切换模式,那么我认为任意 goto 就没有必要了。

Go 编译器没有使用 relooper 算法,因为它本质上与切换 goroutine 的概念不兼容。

确实不能单独应用。 但是,我们使用 wasm 结构化控制流 + Asyncify取得了不错的效果。 这样做的想法是尽可能多地发出正常的 wasm 控制流——ifs、循环等,而不需要一个大的开关——并在该模式之上添加工具来处理堆栈的展开和重绕。 这导致相当小的代码大小,并且非堆栈切换代码可以基本上全速运行,而实际的堆栈切换可能会慢一些(所以这对于堆栈切换不是在每次循环迭代等时不断发生的情况很有用.)。

如果您有兴趣,我很乐意在 Go 上进行实验! 这显然不如 wasm 中内置的堆栈切换支持,但它可能已经比大切换模式更好。 以后切换到内置的堆栈切换支持会更容易。 具体来说,这个实验的工作原理是让 Go 发出正常结构化的代码,根本不用担心堆栈切换,并且只需在适当的点发出对特殊maybe_switch_goroutine函数的调用。 Asyncify 转换将基本上处理所有其余部分。

我对用于动态重新编译模拟器(如 qemu)的 goto 感兴趣。 与其他编译器不同,qemu 根本不了解程序控制流结构,因此 goto 是唯一合理的目标。 尾调用可以通过将每个块编译为函数并将每个 goto 编译为尾调用来解决这个问题。

@kripken感谢您非常有用的帖子。

听起来基线编译器 Liftoff 还可以,问题出在优化编译器 TurboFan 上。 请提交一个问题,或者请提供一个测试用例,如果你愿意,我可以提交一个。

这是一个可以使用wasm_exec.html运行的wasm 二进制文件

你认为 wasm 堆栈切换计划能够解决 Go 的 goroutine 实现问题吗?

是的,乍一看,这似乎会有所帮助。

但是,我们使用 wasm 结构化控制流 + Asyncify 取得了不错的效果。

这看起来也很有希望。 我们需要在 Go 中实现 relooper,但我想这很好。 一个小的缺点是它增加了对 binaryen 的依赖以生成 wasm 二进制文件。 我可能很快就会写一个提案。

我相信 LLVM 的 stackifier 算法更容易/更好,如果你想实现它: https ://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2

我已经提交了 Go 项目的提案: https :

@neelance ,很高兴看到@kripken的建议对 golang + wasm 有所帮助。 考虑到这个问题是 goto/labels 不是堆栈切换的问题之一,并且鉴于 Asyncify 引入了新的 deps / 特殊外壳构建与 Asyncify 直到堆栈切换被释放等 - 你会将其描述为解决方案还是不是最佳缓解? 如果 goto 指令可用,这与估计的收益相比如何?

如果 Linus Torvalds 对链表的 “Good Taste” 论证依赖于删除唯一特殊情况分支语句的优雅,那么很难将这种特殊情况体操视为胜利,甚至是朝着正确方向迈出的一步。 亲自使用 goto 做 C 中的类 async api,谈谈在 goto 指令触发各种异味之前的堆栈切换。

如果我读错了,请纠正我,但除了对所提出的一些问题的关注边缘特殊性的看似轻率的回应外,这里的维护人员似乎没有就手头的问题提供任何澄清,也没有回答棘手的问题。 恕我直言,这种迟缓的僵化不是愈伤组织企业政治的标志吗? 如果是这种情况,我理解困境......想象一下,如果只有 ANSI C 是兼容的试金石,Wasm 品牌可以吹嘘支持的所有语言/编译器!

@neelance @darkuranium @d4tocchini并非所有 Wasm 贡献者都认为缺少 goto 是正确的,事实上,我个人认为这是 Wasm 的 #1 设计错误。 我绝对赞成添加它(作为 funclets 或直接添加)。

然而,在这个线程上进行辩论不会让 goto 发生,也不会神奇地让参与 Wasm 的每个人都相信并为你完成工作。 以下是要采取的步骤:

  1. 加入 Wasm CG。
  2. 有人花时间成为 goto 提案的拥护者。 我建议从现有的 funclets 提案开始,因为@sunfishcode已经深思熟虑它是对当前依赖块结构的引擎和工具的“侵入性最小”,因此它比原始的有更高的成功机会去。
  3. 帮助它通过 4 个提案阶段。 这包括针对您提出的任何反对意见做出好的设计,发起讨论,目的是让足够多的人满意,以便您在通过阶段时获得多数票。

@d4tocchini老实说,我目前将建议的解决方案视为“鉴于我无法改变的情况的最佳前进方式”,也就是“解决方法”。 我仍然认为 jump/goto 指令(或 funclets)是更简单的方式,因此更可取。 (仍然感谢@kripken提供的替代方案。)

@aardappel据我所知, @sunfishcode试图推动

@neelance我认为@sunfishcode没有太多时间来推动提案超出其最初的创建,因此它是“停滞不前”而不是“失败”。 正如我试图指出的那样,它需要一个拥护者持续不断地工作,以使提案一直通过管道。

@neelance

感谢您的测试用例! 我可以在本地确认同样的问题。 我提交了https://bugs.chromium.org/p/v8/issues/detail?id=11237

我们需要在 Go [..] 中实现 relooper 一个小的缺点是它添加了对 binaryen 的依赖以生成 wasm 二进制文件。

顺便说一句,如果有帮助,我们可以将 binaryen 库构建为单个 C 文件。 也许这更容易集成?

此外,使用 Binaryen,您可以使用那里的 Relooper 实现。 您可以将基本的 IR 块传递给它并让它重新循环。

@taralx

我相信 LLVM 的 stackifier 算法更容易/更好,

请注意,该链接与上游 LLVM 无关,它是 Cheerp 编译器(它是 LLVM 的一个分支)。 他们的 Stackifier 与 LLVM 的名称相似,但不同。

另请注意,Cheerp 帖子指的是 2011 年的原始算法 -现代 relooper 实现(如前所述)多年来一直没有他们提到的问题。 我不知道该通用方法的更简单或更好的替代方法,这与 Cheerp 和其他人所做的非常相似 - 这些是主题的变体。

@kripken感谢您提出问题。

顺便说一句,如果有帮助,我们可以将 binaryen 库构建为单个 C 文件。 也许这更容易集成?

不太可能。 Go 编译器本身已在不久前转换为纯 Go 并且 afaik 它不使用其他 C 依赖项。 我不认为这将是一个例外。

以下是 funclets 提案的当前状态:流程的下一步是要求 CG 投票进入第一阶段。

我本人目前专注于 WebAssembly 的其他领域,没有足够的带宽来推动 funclet 向前发展; 如果有人有兴趣接替 funclets 的冠军角色,我很乐意将其交给。

不太可能。 Go 编译器本身已在不久前转换为纯 Go 并且 afaik 它不使用其他 C 依赖项。 我不认为这将是一个例外。

此外,这并没有解决在 WebAssembly 运行时中大量使用 relooper 导致严重性能悬崖的问题。

@Vurich

我认为这可能是将 goto 添加到 wasm 的最佳案例,但有人需要从显示如此严重的性能悬崖的真实代码中收集令人信服的数据。 我自己还没有看到这样的数据。 分析 wasm 性能缺陷的工作,例如“Not So Fast: Analyzing 不是由于结构化的控制流 - 而是由于安全检查)。

@kripken您对如何收集此类数据有任何建议吗? 如何证明性能不足是由于结构化控制流造成的?

不太可能有很多工作来分析编译阶段的性能,这是这里抱怨的一部分。

我有点惊讶我们还没有 switch case 构造,但是 funclets 包含了它。

@neelance

要弄清楚具体原因并不容易,是的。 例如边界检查,您可以在 VM 中禁用它们并对其进行测量,但遗憾的是,没有一种简单的方法可以对 goto 执行相同的操作。

一种选择是手动比较发出的机器代码,这就是他们在链接的论文中所做的。

另一种选择是将wasm编译为您认为可以最佳处理控制流的东西,即“撤消”结构。 LLVM 应该能够做到这一点,因此在使用 LLVM(如 WAVM 或 wasmer)或通过 WasmBoxC 的 VM 中运行 wasm 可能会很有趣。 您也许可以在 LLVM 中禁用 CFG 优化,看看这有多重要。

@taralx

有趣的是,我错过了一些关于编译时间或内存使用的东西吗? 结构化控制流实际上应该在那里更好 - 例如,与一般 CFG 相比,从那里进入 SSA 形式非常简单。 这实际上是 wasm 首先采用结构化控制流的原因之一。 这也是非常仔细地测量的,因为它会影响 Web 上的加载时间。

(或者您的意思是开发人员机器上的编译器性能?确实,wasm 确实倾向于在那里做更多的工作,而在客户端上做的工作更少。)

我的意思是在嵌入器中编译性能,但似乎这被视为错误,不一定是纯粹的性能问题?

@taralx

是的,我认为这是一个错误。 它只发生在一个虚拟机的一层中。 并且没有根本原因 - 结构化控制流不需要更多资源,它应该需要更少。 也就是说,我敢打赌,如果 wasm 确实有 goto,那么这种性能错误将更有可能发生。

@克里普肯

结构化控制流实际上应该在那里更好 - 例如,与一般 CFG 相比,从那里进入 SSA 形式非常简单。 这实际上是 wasm 首先采用结构化控制流的原因之一。 这也是非常仔细地测量的,因为它会影响 Web 上的加载时间。

一个非常具体的问题,以防万一:你知道任何真正做到这一点的 Wasm 编译器 - 从“结构化控制流”到 SSA 形式的“非常简单”。 因为快速浏览一下,Wasm 的控制流并不是(完全/最终)结构化的。 正式结构化控制是没有break s、 continue s、 return s 的控制(大致是 Scheme 的编程模型,没有像 call/cc 这样的魔法)。 当这些存在时,这种控制流大致可以称为“半结构化”。

对于完全结构化的控制流,有一个众所周知的 SSA 算法: http ://citeseerx.ist.psu.edu/viewdoc/summary?doi=

对于结构化语句,我们展示了如何在解析过程中一次生成 SSA 表单和支配树。 在下一节中,我们将展示甚至可以将我们的方法扩展到某一类非结构化语句(LOOP/EXIT 和 RETURN),这些语句可能会导致在任意点从控制结构中退出。 但是,由于此类退出是一种(有纪律的)goto,因此它们比结构化语句更难处理也就不足为奇了。

OTOH,还有另一种众所周知的算法, https: //pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf 可以说也是单通道,但不仅在非结构化控制方面没有问题流,但即使是不可约的控制流(尽管它不会产生最佳结果)。

所以,问题又是你是否知道某些项目经历了实际扩展 Brandis/Mössenböck 算法的麻烦,并且与 Braun 等人相比,在这条路线上取得了实实在在的好处。 算法(作为旁注,我的直觉预感是布劳恩算法正是这样一个“上限”扩展,虽然我太笨了,无法直观地向自己证明它,而不是在谈论正式的证明,所以就是这样 - 直觉预感)。

问题的一般主题是确定(尽管我会说“保持”)Wasm 选择退出任意 goto 支持的最终原因。 因为看了这个帖子多年,我建立的心智模型是为了避免面对不可约的CFG。 事实上,鸿沟谎言还原束缚CFGS之间,有许多优化算法是(多)为还原CFGS更容易,并在有编码了许多优化是的。在WASM(半)结构化控制流只是一种廉价的方式,以保证可还原性。

提到结构化 CFG 的 SSA 生产的任何特殊容易性(而 Wasm CFG 似乎在正式意义上并不是真正结构化的)以某种方式掩盖了上面的清晰画面。 这就是为什么我要问是否有具体的参考资料表明 SSA 建设实际上受益于 Wasm CFG 表格。

谢谢。

@kripken我现在有点困惑,渴望学习。 我正在查看情况,目前我看到以下内容:


你的程序的源码有一定的控制流程。 这个 CFG 要么可约化,要么不可约化,例如 goto 是否已在源语言中使用。 没有办法改变这个事实。 该 CFG 可以转换为机器代码,例如 Go 编译器本身所做的那样。

如果 CFG 已经是可约化的,那么一切都很好,wasm VM 可以快速加载它。 任何翻译通行证都应该能够检测到这是简单的情况并做快速的事情。 允许不可约 CFG 不应减慢这种情况。

如果 CFG 不可约,则有两种选择:

  • 编译器使其可简化,例如通过引入跳转表。 此步骤会丢失信息。 如果没有特定于生成二进制文件的编译器的分析,就很难恢复原始 CFG。 由于这种信息丢失,生成的任何机器代码都会比从初始 CFG 生成的代码慢一些。 我们也许可以用单遍算法生成这个机器码,但这是以信息丢失为代价的。 [1]

  • 我们允许编译器发出不可约的 CFG。 VM 可能必须使其可简化。 这会减慢加载时间,但仅限于 CFG 实际上不可约的情况。 编译器可以选择优化加载时性能或运行时性能。

[1] 我知道如果仍有某种方法可以逆转操作,这并不是真正的信息丢失,但我无法用更好的方式来描述它。


我的思维缺陷在哪里?

@pfalcon

您是否知道任何真正做到这一点的 Wasm 编译器 - 从“结构化控制流”到 SSA 形式的“非常简单”。

关于虚拟机:我不直接知道。 但 IIRC 早在@titzer@lukewagner 就表示,以这种方式实施很方便 - 也许其中一个可以详细说明。 我不确定不可约性是否是整个问题。 而且我不确定他们是否实现了你提到的那些算法。

关于 VM 以外的东西:Binaryen 优化器肯定受益于 wasm 的结构化控制流,而不仅仅是它是可简化的。 各种优化都更简单,因为我们总是知道循环头在哪里,例如,在 wasm 中注释了哪些。 (OTOH 其他优化更难做,我们确实有一个通用的 CFG IR 以及那些......)

@neelance

如果 CFG 已经是可约化的,那么一切都很好,wasm VM 可以快速加载它。 任何翻译通行证都应该能够检测到这是简单的情况并做快速的事情。 允许不可约 CFG 不应减慢这种情况。

也许我没有完全理解你。 但是,wasm VM 能否快速加载代码不仅取决于它是否可简化,还取决于它的编码方式。 具体来说,我们可以设想一种通用 CFG 格式,然后 VM 需要进行工作以验证它是可简化的。 Wasm 选择避免这项工作 - 编码必然是可简化的(也就是说,当您阅读 wasm 并进行琐碎的验证时,您也在证明它是可简化的,而无需做任何额外的工作)。

此外,wasm 的编码不仅提供了可简化性的保证,而且无需验证。 它还注释循环头、ifs 和其他有用的东西(正如我在本评论前面单独提到的那样)。 我不确定有多少生产虚拟机从中受益,但我希望他们会这样做。 (也许尤其是在基线编译器中?)

总的来说,我认为允许不可约的 CFG 可以减慢快速情况,除非不可约的 CFG 以单独的方式编码(如建议的 funclet)。

@克里普肯

谢谢你的解释。

是的,这正是我想要做出的区分:我看到了结构化符号/编码对于可简化 CFG 案例的优势。 但是添加一些允许使用不可约 CFG 表示法的结构应该不难,并且在可约源 CFG 的情况下仍然保持现有优势(例如,如果您不使用这个新结构,那么 CFG 是有保证的可还原)。

作为结论,我不明白人们怎么能说纯可约符号更快。 在可还原源 CFG的情况下,它同样快。 在不可约源 CFG的情况下,最多可以说它并没有明显变慢,但是一些现实世界的案例已经表明,一般情况下不太可能出现这种情况。

简而言之,我看不出性能考虑如何成为阻止不可约控制流的论据,这让我质疑为什么下一步需要收集性能数据。

@neelance

是的,我同意我们可以添加一个新的结构——比如 funclets——并且不使用它,它不会减慢现有案例的速度。

但是添加任何新构造都有一个缺点,因为它增加了 wasm 的复杂性。 特别是,这意味着 VM 上的表面积更大,这意味着更多可能的错误和安全问题。 Wasm 倾向于在开发人员方面尽可能地增加复杂性,以降低 VM 的复杂性。

一些 wasm 提案不仅仅是关于速度,比如 GC(它允许使用 JS 进行循环收集)。 但是对于与速度有关的提案,比如 funclet,我们需要证明速度证明了复杂性是合理的。 我们就 SIMD 进行了一场关于速度的辩论,并认为这是值得的,因为我们看到它可以可靠地在实际代码上实现非常大的加速(2 倍甚至更多)。

(我同意,除了允许通用 CFG 的速度之外,还有其他好处,比如让编译器更容易以 wasm 为目标。但我们可以在不增加 wasm VM 复杂性的情况下解决这个问题。我们已经在 LLVM 和 Binaryen 中提供了对任意 CFG 的支持,允许编译器发出 CFG 而不必担心结构化控制流。如果这还不够好,我们——我的意思是工具人,包括我——应该做更多。)

Funclet 并不是关于速度,而是关于允许具有非平凡控制流的语言编译为 WebAssembly,C 和 Go 是最明显的,但它适用于任何具有 async/await 的语言。 此外,选择分层控制流实际上会导致 VM 中出现更多错误,这可以从除 V8 之外的所有 Wasm 编译器将分层控制流分解为 CFG 的事实证明。 CFG 中的 EBB 可以表示 Wasm 中的多个控制流结构等等,并且与具有不同用途的许多不同种类相比,编译单个结构导致的错误要少得多。

即使是 Lightbeam,一个非常简单的流式编译器,在添加一个将控制流分解为 CFG 的额外翻译步骤后,错误编译错误也大大减少。 这对于这个过程的另一面来说是双倍的——Relooper 比发出 funclet 更容易出错,而且我被开发人员告诉我,他们正在为 LLVM 和其他编译器开发 Wasm 后端,这些编译器是要实现的 funclet,它们会发出每个函数单独使用funclets,以提高codegen的可靠性和简单性。 所有生产 Wasm 的编译器都使用 EBB,除了一个使用 Wasm 的编译器之外的所有编译器都使用 EBB,这种拒绝实现 funclet 或其他表示 CFG 的方式只是在两者之间增加了一个有损步骤,这会损害除 V8 团队之外的所有相关方.

“不可约控制流被认为是有害的”只是一个话题,您可以轻松添加 funclets 的控制流是可约化的限制,然后如果您希望将来允许不可约化的控制流,所有现有的具有可约化控制流的 Wasm 模块都可以不加修改地工作在另外支持不可约控制流的引擎上。 这只是在验证器中删除可还原性检查的情况。

@Vurich

您可以轻松添加 funclets 的控制流可简化的限制

你可以,但这不是微不足道的——虚拟机需要验证这一点。 我认为这在单个线性传递中是不可能的,这对于现在大多数 VM 中都存在的基线编译器来说将是一个问题。 (事实上​​,仅仅找到循环后缘——这是一个更简单的问题,并且出于其他原因也是必要的——不能在一次前向传递中完成,不是吗?)

无论如何,除 V8 之外的所有 Wasm 编译器都将分层控制流分解为 CFG。

您指的是 TurboFan 使用的“节点海”方法吗? 我不是这方面的专家,所以我会留给其他人来回答。

但更一般地说,即使您不购买上述优化编译器的论点,如前所述,对于基线编译器来说,它甚至更直接正确。

Funclet 并不是关于速度,而是关于允许具有非平凡控制流的语言编译为 WebAssembly [..] Relooper 比发出 funclet 更容易出错

我同意 100% 在工具方面。 从大多数编译器发出结构化代码更难! 但关键是它使 VM 端变得更简单,这就是 wasm 选择做的事情。 但同样,我同意这有权衡,包括你提到的缺点。

2015 年的时候,wasm 是不是搞错了? 这是可能的。 我认为我们自己做错了一些事情(比如可调试性,以及后期切换到堆栈机器)。 但回想起来不可能解决这些问题,并且添加新事物的门槛很高,尤其是重叠的事物。

考虑到这一切,为了具有建设性,我认为我们应该解决工具方面的现有问题。 工具更改的门槛要低得多。 两个可能的建议:

  • 我可以考虑将 Binaryen CFG 代码移植到 Go,如果这有助于 Go 编译器 - @neelance
  • 我们可以纯粹在工具方面实现 funclet 或类似的东西。 也就是说,我们今天为此提供了库代码,但也可以添加二进制格式。 (在工具方面,在 wasm 目标文件中添加到 wasm 二进制格式已经有先例。)

我们可以纯粹在工具方面实现 funclet 或类似的东西。 也就是说,我们今天为此提供了库代码,但也可以添加二进制格式。 (在工具方面,在 wasm 目标文件中添加到 wasm 二进制格式已经有先例。)

如果对此有任何具体工作,值得注意的是(AFAIU)将其添加到 Wasm 的最小惯用方式(正如@rossberg 所暗示的那样)是引入块指令

循环(t out (_instr_* end ) _n_

它定义了 n 个带标签的主体(带有 n 个前向声明的输入类型注释)。 然后对br指令系列进行泛化,以便多循环定义的所有标签都在每个主体的范围内,按顺序排列(如,任何主体都可以从任何其他主体中分支出来)。 当一个循环体被分支到时,执行跳转到体的_start_(就像一个常规的 Wasm 循环)。 当执行到达一个主体的末尾而没有分支到另一个主体时,整个构造返回(没有失败)。

关于如何有效地表示每个主体的类型注释,会有一些事情要做(在上面的公式中,n 个主体可以有 n 个不同的输入类型,但必须都具有相同的输出类型,所以我不能直接使用规则的多值_blocktype_索引,而不需要多余的感觉LUB计算),以及如何选择要执行的初始主体(总是第一个,还是应该有一个静态参数?)。

这获得了与 funclet 相同级别的表达能力,但避免了引入新的控制指令空间。 事实上,如果 funclets 被进一步迭代,我认为它会变成这样的东西。

编辑:将其调整为具有失败行为会使形式语义稍微复杂化,但对于@neelance的用例可能会更好,并且可以帮助向基线编译器提示跟踪控制流路径是什么。

Wasm 将工作卸载到工具上以使引擎更简单/更快的设计原则非常重要,并将继续非常有益。

也就是说,就像所有重要的事情一样,它是一种权衡,而不是非黑即白。 我相信这里有一个案例,生产者的痛苦与引擎的痛苦不成比例。 我们希望引入 Wasm 的大多数编译器要么在内部使用任意 CFG 结构(SSA),要么用于针对不介意 goto 的东西(CPU)。 我们正在让世界跳过铁环,但收获并不大。

funclets(或多循环)之类的东西很好,因为它是模块化的:如果生产者不需要它,那么一切都会像以前一样工作。 如果一个引擎真的不能处理任意的 CFG,那么目前他们可以发出它,就好像它是一个loop + br_table类型的构造,只有那些使用它的人付出代价. 然后,“市场决定”,我们看看引擎是否有压力为其发出更好的代码。 有些事情告诉我,如果有很多 Wasm 代码依赖于 funclet,那么引擎为它们发出好的代码真的不会像某些人认为的那样是一场大灾难。

你可以,但这不是微不足道的——虚拟机需要验证这一点。 我认为这在单个线性传递中是不可能的,这对于现在大多数 VM 中都存在的基线编译器来说将是一个问题。

也许我误解了对基线编译器的期望,但他们为什么要关心? 如果看到 goto,请插入跳转指令。

我同意 100% 在工具方面。 从大多数编译器发出结构化代码更难! 但关键是它使 VM 端变得更简单,这就是 wasm 选择做的事情。 但同样,我同意这有权衡,包括你提到的缺点。

不,正如我在原始评论中多次说过的那样,它_不会_让虚拟机方面的事情变得更容易。 我在基线编译器上工作了一年多,在我添加了一个将 Wasm 的控制流转换为 CFG 的中间步骤后,我的生活变得更加轻松,并且发出的代码变得更快。

你可以,但这不是微不足道的——虚拟机需要验证这一点。 我认为这在单个线性传递中是不可能的,这对于现在大多数 VM 中都存在的基线编译器来说将是一个问题。 (事实上​​,仅仅找到循环后缘——这是一个更简单的问题,并且出于其他原因也是必要的——不能在一次前向传递中完成,不是吗?)

好的,事情就是这样,我对编译器中使用的算法的了解还不够强,无法绝对肯定地说明在流编译器中可以或不能检测到不可约控制流,但事实是它不需要。 验证可以与编译同时进行。 如果不存在流式算法(您和我都不知道它不存在),则可以在完全接收到函数后使用非流式算法。 如果(由于某种原因)不可简化的控制流导致像无限循环这样的真正糟糕的事情,您可以简单地使编译超时和/或取消编译线程。 但是,没有理由相信会是这样。

也许我误解了对基线编译器的期望,但他们为什么要关心? 如果看到 goto,请插入跳转指令。

这不是那么简单,因为您需要如何将 Wasm 的无限寄存器机器(不,它不是堆栈机器)映射到物理硬件的有限寄存器,但这是任何流编译器都必须解决的问题,并且它完全正交于CFG 与分层控制流。

我工作的流编译器可以编译任意的——甚至是不可约的——CFG。 它没有做任何特别的事情。 当您第一次需要跳转到每个块时,您只需为每个块分配一个“调用约定”(基本上是该块中范围内的值应该所在的位置),并且如果您到达需要有条件地分支到两个的点或更多具有不兼容“调用约定”的目标,您将“适配器”块推送到队列并在下一个可能的点发出它。 这可能发生在可简化和不可简化的控制流中,并且在任何一种情况下都几乎没有必要。 正如我之前所说,“被认为是有害的不可约控制流”的论点是一个话题,而不是一个技术论点。 将控制流表示为 CFG 使得流式编译器更容易编写,正如我多次说过的,我从丰富的个人经验中知道这一点。

任何无法简化的控制流使实现更难编写的情况(我认为没有)都可以被剔除并返回错误,如果您需要一个单独的非流式算法来 100% 确定地检测该控制流是不可约的(所以你不会意外地接受不可约的控制流),那么它可以与基线编译器本身分开运行。 有人告诉我,我有理由相信他是该主题的权威(尽管我会避免调用他们,因为我知道他们不想被拖入这个线程)存在一个相对简单的流算法用于检测 CFG 的不可约性,但我不能直接说这是真的。

@oridb

也许我误解了对基线编译器的期望,但他们为什么要关心? 如果看到 goto,请插入跳转指令。

基线编译器仍然需要做一些事情,比如在循环后端插入额外的检查(这就是在 Web 上一个挂起的页面最终会显示一个缓慢的脚本对话的方式),所以他们需要识别这样的事情。 此外,他们确实尝试进行合理有效的寄存器分配(基线编译器的运行速度通常是优化编译器的 1/2 左右——考虑到它们是单通道的,这非常令人印象深刻!)。 拥有控制流的结构,包括连接和拆分,使这变得容易得多。

@gwvo

也就是说,就像所有重要的事情一样,它是一种权衡,而不是非黑即白。 [..] 我们正在让世界通过篮球跳跃而不是获得太多收益。

完全同意这是一个权衡,甚至可能当时 wasm 弄错了。 但我相信在工具方面修复这些箍要实用得多。

然后,“市场决定”,我们看看引擎是否有压力为其发出更好的代码。

这实际上是我们迄今为止避免的事情。 我们试图在 VM 上使 wasm 尽可能简单,因此它不需要复杂的优化——甚至尽可能地不需要像内联这样的东西。 目标是在工具方面做艰苦的工作,而不是迫使 VM 做得更好。

@Vurich

我在基线编译器上工作了一年多,在我添加了一个将 Wasm 的控制流转换为 CFG 的中间步骤后,我的生活变得更加轻松,并且发出的代码变得更快。

很有意思! 那是哪个虚拟机?

我也会特别好奇它是否是单通道/流式传输(如果是,它是如何处理循环后端检测的?),以及它是如何进行寄存器分配的。

原则上,循环后沿和寄存器分配都可以基于线性指令顺序进行处理,期望基本块将按某种合理的类似 topsort 的顺序放置,而不是严格要求它。

对于循环后沿:将后沿定义为在指令流中跳转到更早的指令。 在最坏的情况下,如果块是向后布局的,你会得到比严格需要的更多的后端检查。

对于寄存器分配:这只是标准的线性扫描寄存器分配。 变量的寄存器分配生命周期从第一次提及变量到最后一次提及,包括介于两者之间的所有块。 在最坏的情况下,如果块被洗牌,你会得到比需要的更长的生命周期,从而不必要地将东西溢出到堆栈中。 唯一的额外成本是跟踪每个变量的第一次和最后一次提及,这可以通过一次线性扫描对所有变量进行。 (对于 wasm,我认为“变量”是本地或堆栈插槽。)

@克里普肯

我可以考虑将 Binaryen CFG 代码移植到 Go,如果这有助于 Go 编译器 - @neelance

用于集成 Asyncify? 请对该提案发表评论。

@comex

好点!

唯一的额外成本是跟踪每个变量的第一次和最后一次提及

是的,我认为这是一个显着的区别。 线性扫描寄存器分配比wasm 基线编译器当前所做的更好(但做起来更慢),因为它们以非常快的流式方式编译。 也就是说,没有找到每个变量的最后提及的初始步骤 - 它们在一次传递中编译,在运行时发出代码,甚至在之后的 wasm 函数中都看不到代码,这得益于结构的帮助,而且它们也变得简单他们去的选择(“愚蠢”是那个帖子中使用的词)。

如果允许块相互递归(如在 https://github.com/WebAssembly/design/issues/796#issuecomment-742690194 中),V8 的寄存器分配流式处理方法应该同样有效,因为它们处理的唯一生命周期绑定在单个块(堆栈)内或假定为函数范围(本地)。

IIUC(参考@titzer评论)V8 的主要问题在于 Turbofan 可以优化的 CFG 类型。

@克里普肯

我们试图在 VM 上使 wasm 尽可能简单,因此它不需要复杂的优化

这不是“复杂的优化”.. goto 对于许多系统来说是非常基本和自然的。 我敢打赌,有很多引擎可以免费添加它。 我只是说,如果有引擎出于任何原因想要保留结构化 CFG 模型,他们可以。

例如,我很确定 LLVM(目前是我们排名第一的 Wasm 生产者)不会切换到使用 funclet,直到确信它不是主要引擎中的性能回归。

谢谢@Vurich ,很有趣。 看到可用的性能数字会很棒,尤其是对于启动和吞吐量。 我猜你的方法会比 V8 和 SpiderMonkey 工程师采用的方法编译得更慢,同时生成更快的代码。 所以在这个领域是一个不同的权衡。 正如您所说,您的方法似乎没有从 wasm 的结构化控制流中受益,而他们的方法确实如此。

不,它是一个流编译器,并且比这两个引擎中的任何一个都更快地发出代码(尽管在我离开项目时还没有解决退化的情况)。 虽然我尽了最大努力来发出快速代码,但它的主要设计目的是快速发出代码,输出效率是次要的问题。 据我所知,启动成本为零(高于 Wasmtime 在后端之间共享的固有成本),因为每个数据结构都未初始化并且编译是按指令完成的。 虽然我手头没有数据可与 V8 或 SpiderMonkey 进行比较,但我确实有数据可与 Cranelift(wasmtime 中的主要引擎)进行比较。 在这一点上它们已经过时了几个月,但你可以看到它不仅比 Cranelift 发出代码的速度更快,而且它发出的代码也比 Cranelift 更快。 当时,它还发出了比 SpiderMonkey 更快的代码,尽管你必须相信我的话,所以如果你不相信我,我不会怪你。 虽然我手头没有最新的数据,但我相信现在的状态是,与 Lightbeam 相比,Cranelift 和 SpiderMonkey 都修复了少量错误,这些错误是它们在这些微基准测试中表现不佳的主要来源,但是编译速度差异在我参与该项目的整个过程中并没有改变,因为每个编译器的基本架构仍然相同,并且是各自的架构导致了不同的性能水平。 虽然我很欣赏您的推测,但我不知道您认为我概述的方法会更慢的假设来自哪里。

这是基准, ::compile基准是针对编译速度的, ::run基准是针对机器代码输出的执行速度的。 https://gist.github.com/Vurich/8696e67180aa3c93b4548fb1f298c29e

方法在这里,您可以克隆它并重新运行基准测试以自己确认结果,但 PR 可能与最新版本的 wasmtime 不兼容,因此它只会向您显示我上次更新时的性能比较公关。 https://github.com/bytecodealliance/wasmtime/pull/1660

话虽如此,我的论点是_不_ CFG 是流编译器中性能的有用内部表示。 我的论点是 CFG 不会对任何编译器的性能产生负面影响,当然也不会达到完全阻止 GCC 和 Go 团队生产 WebAssembly 的程度。 在这个线程中,几乎没有人反对 funclet 或对 wasm 的类似扩展,实际上他们声称会受到该提案的负面影响的项目。 并不是说你需要第一手的经验来评论这个话题,我认为每个人都有一定程度的有价值的投入,但就是说对车棚的颜色有不同的看法和制作之间有一条线声称无非是空洞的猜测。

@Vurich

不,它是一个流式编译器,并且比这两个引擎中的任何一个都更快地发出代码(尽管有一些退化的情况因为我离开了项目而从未修复)。

抱歉,如果我之前不够清楚。 为了确保我们谈论的是同一件事,我指的是那些引擎中的基线编译器。 我说的是编译时间,在 V8 和 SpiderMonkey 使用该术语的意义上,这是基线编译器的重点。

我怀疑您能否超越 V8 和 SpiderMonkey 基线编译时间的原因是,正如我之前提供链接中那样,这两个基线编译器针对编译时间进行了特别调整。 特别是它们不生成任何内部 IR,它们只是直接从 wasm 到机器代码。 您说您的编译器确实发出了内部 IR(对于 CFG) - 我希望您的编译时间会因此而变慢(由于更多的分支、内存带宽等)。

但是请对那些基线编译器进行基准测试! 我很乐意看到数据显示我的猜测是错误的,我相信 V8 和 SpiderMonkey 工程师也会如此。 这意味着您找到了他们应该考虑采用的更好的设计。

要针对 V8 进行测试,您可以运行d8 --liftoff --no-wasm-tier-up ,而对于 SpiderMonkey,您可以运行sm --wasm-compiler=baseline

(感谢您提供与 Cranelift 进行比较的说明,但 Cranelift 不是基线编译器,因此在这种情况下比较编译时间与它无关。不过,非常有趣,我同意。)

我的直觉是基线编译器不必显着改变他们的编译策略来支持 funclets/ multiloop ,因为他们无论如何都不会尝试进行有意义的块间优化。 @kripken引用的依赖的“控制流结构,包括连接和拆分”通过要求对相互递归块集合的所有输入类型进行前向声明来满足(这似乎是流验证的自然选择) . Lightbeam/Wasmtime 是否可以击败引擎基线编译器并没有考虑到这一点; 重要的一点是引擎基线编译器是否可以保持与现在一样快。

FWIW,我很想看到这个功能在未来的 CG 会议上被提出来讨论,我广泛同意@Vurich 的观点,如果引擎代表不准备实施它,他们可以自己反对。 话虽如此,我们应该认真对待任何此类异议(我之前曾在面对面的会议上表示,在追求此功能时,我们应该尽量避免 JavaScript Proper Tail Calls saga的 WebAssembly 版本)。 一旦我完成了我(目前非常迟)的论文提交,我很高兴自己在新的一年里就这样的 CG 讨论迈出第一步。

@克里普肯

是的,我认为这是一个显着的区别。 线性扫描寄存器分配比wasm 基线编译器当前所做的更好(但做起来更慢),因为它们以非常快的流式方式编译。 也就是说,没有找到每个变量的最后提及的初始步骤 - 它们在一次传递中编译,在运行时发出代码,甚至在之后的 wasm 函数中都看不到代码,这得益于结构的帮助,而且它们也变得简单他们去的选择(“愚蠢”是那个帖子中使用的词)。

哇,这真的很简单。

另一方面……该特定算法非常简单,以至于它不依赖于结构化控制流的任何深层属性。 它甚至几乎不依赖于结构化控制流的浅层属性。

正如博客文章所提到的,SpiderMonkey 的 wasm 基线编译器不通过“控制流连接”(即具有多个前驱的基本块)来保留寄存器分配器状态,而是使用固定的 ABI,或者从 wasm 堆栈映射到本机堆栈和寄存器. 我通过测试发现它在输入 blocks 时也使用了固定的 ABI,即使在大多数情况下这不是控制流连接!

固定的 ABI 如下(在 x86 上):

  • 如果有非零参数(进入块时)或返回(退出块时),则 wasm 堆栈的顶部进入rax ,其余的 wasm 堆栈对应于 x86堆。
  • 否则,整个 wasm 堆栈对应 x86 堆栈。

为什么这很重要?

因为这个算法可以用更少的信息以几乎相同的方式工作。 作为一个思想实验,想象一下 WebAssembly 的替代宇宙版本,其中没有结构化的控制流指令,只有跳转指令,类似于原生汇编。 它只需要增加一条额外的信息:一种判断哪些指令是跳转目标的方法。

那么算法就是:线性地执行指令; 在跳转和跳转目标之前,将寄存器刷新到固定的 ABI。

一个区别是必须有一个固定的 ABI,而不是两个。 它无法区分栈顶值在语义上是跳转的“结果”,还是从外部块留在栈中。 所以它必须无条件地将栈顶放入rax

但我怀疑这会对性能产生任何可衡量的成本。 如果有的话,它可能是一个改进。

(验证也会有所不同,但仍然是单次通过。)

好的,前面的警告:

  1. 这不是另一个宇宙。 我们坚持对现有的 WebAssembly 进行向后兼容的扩展。
  2. SpiderMonkey 的基线编译器只是一种实现,它在寄存器分配方面可能不是最理想的:如果它更聪明一点,运行时的好处将超过编译时的成本。
  3. 即使基线编译器不需要额外的信息,优化编译器也可能需要它来快速构建 SSA。

考虑到这些,上述思想实验加强了我的信念,即基线编译器不需要结构化控制流。 无论我们添加了多么低级的构造,只要它包含诸如哪些指令是跳转目标的基本信息,基线编译器只需进行微小的更改即可处理它。 或者至少这个可以。

@康拉德瓦特@comex

这些都是非常好的点! 我对基线编译器的直觉很可能是错误的。

@comex - 是的,正如你所说,这个讨论与优化编译器是分开的,SSA 可能会从结构中受益。 也许值得从之前的一个链接中引用一下:

按照设计,以简单的单通道将 WebAssembly 代码转换为 TurboFan 的 IR(包括 SSA 构造)非常有效,部分原因在于 WebAssembly 的结构化控制流。

@conrad-watt 我绝对同意我们只需要从 VM 人员那里获得直接反馈,并保持开放的心态。 需要明确的是,我的目标不是阻止任何事情。 我在这里详细评论,因为有几条评论似乎认为 wasm 的结构化控制流是一个明显的错误,或者显然应该用 funclets/multiloop 来纠正——我只是想在这里介绍一下思想的历史,并且有充分的理由对于当前模型,因此可能不容易改进。

我真的很喜欢阅读这段对话。 我自己也想知道一堆这样的问题(来自两个方向),并分享了许多这些想法(再次来自两个方向),讨论提供了很多有用的见解和经验。 我不确定我是否有强烈的意见,但我有一个想法可以在每个方向上做出贡献。

在“for”方面,预先知道哪些块有后边很有用。 流式编译器可以跟踪 WebAssembly 类型系统中不明显的属性(例如,本地i的索引在本地arr中的数组边界内)。 向前跳转时,使用该点所具有的属性来注释目标可能很有用。 这样,当到达标签时,可以使用包含所有入边的属性来编译其块,例如消除数组边界检查。 但是如果一个标签可能有一个未知的背景,那么它的块就不能用这个知识来编译。 当然,非流式编译器可以进行一些更重要的循环不变分析,但对于流式编译器来说,不必担心可能会发生什么是很有用的。 (旁白局部变量。在 #1381 中,我列出了一些减少对

在“反对”方面,到目前为止,讨论只集中在地方控制上。 这对 C 来说很好,但是对于 C++ 或其他各种具有类似例外的语言呢? 具有其他形式的非本地控制的语言呢? 具有动态范围的事物通常具有固有的结构(或者至少我不知道任何相互递归的动态范围的示例)。 我认为这些考虑因素是可以解决的,但是您必须在设计时考虑到它们,才能使结果在这些设置中可用。 这是我一直在思考的事情,我很高兴与任何感兴趣的人分享我正在进行的想法(看起来大致像@conrad-watt 的多循环的扩展)(虽然这里似乎离题了),但是我想至少提醒一下,要记住的不仅仅是本地控制流。

(尽管我认为@kripken在代表这些考虑方面做得很好,但我意见。)

当我说 Lightbeam 产生内部 IR 时,这确实具有误导性,我应该澄清一下。 我在这个项目上工作了一段时间,有时你可以得到隧道视野。 基本上,Lightbeam 逐条使用输入指令(它实际上最多有一条指令前瞻,但这并不是特别重要),并且对于它在恒定空间中懒惰地产生的每条指令,都有许多内部 IR 指令。 每个 Wasm 指令的最大指令数是恒定的并且很小,例如 6。它不是为整个函数创建一个 IR 指令缓冲区并进行处理。 然后,它会一一读取这些 IR 指令。 你真的可以把它想象成一个更通用的辅助函数库,它实现了每个 Wasm 指令,我只是将它称为 IR,因为这有助于解释它如何具有不同的控制流模型等。它可能不会像 V8 或 SpiderMonkey 的基线编译器那样快速生成代码,但那是因为它没有完全优化,而不是因为它在架构上存在缺陷。 我的观点是,我在内部对 Wasm 的分层控制流进行建模,就好像它是一个 CFG,而不是像 LLVM 或 Cranelift 那样在内存中实际生成一个 IR 缓冲区。

另一种选择是将wasm编译为您认为可以最佳处理控制流的东西,即“撤消”结构。 LLVM 应该能够做到这一点,因此在使用 LLVM(如 WAVM 或 wasmer)或通过 WasmBoxC 的 VM 中运行 wasm 可能会很有趣。

@kripken不幸的是,LLVM 似乎还无法撤消结构。 跳转线程优化通道应该可以做到这一点,但还不能识别这种模式。 下面是一个示例,展示了一些 C++ 代码,它模拟了 relooper 算法如何将 CFG 转换为 loop+switch。 GCC 设法“dereloop”它,但 clang 没有: https ://godbolt.org/z/GGM9rP

@AndrewScheidecker有趣,谢谢。 是的,这些东西可能非常难以预测,因此可能没有比调查发出的代码更好的选择(正如前面链接的“No So Fast”论文所做的那样),并避免尝试使用捷径,例如依赖 LLVM 的优化器。

@comex

SpiderMonkey 的基线编译器只是一种实现,它在寄存器分配方面可能不是最理想的:如果它更聪明一点,运行时的好处将超过编译时的成本。

显然,寄存器分配可能更聪明。 它在控制流分叉、连接和调用之前不加选择地溢出,并且可以维护有关寄存器状态的更多信息,并尝试将寄存器中的值保留更长时间/直到它们死亡。 它可以为块的值结果选择比 rax 更好的寄存器,或者更好的是,不使用固定寄存器。 它可以静态地使用几个寄存器来保存局部变量; 我所做的语料库分析表明,对于大多数功能来说,只需几个整数和 FP 寄存器就足够了。 一般来说,溢出可能更聪明; 事实上,当它用完寄存器时,它会惊慌失措。

这样做的编译时间成本主要是每个控制流边缘将具有与其相关联的非恒定信息量(寄存器状态),这可能导致更普遍地使用动态存储分配,基线编译器如此远远避开。 当然,在每个连接(和其他地方)处理可变大小的信息会产生相关成本。 但是已经有一些非常量的成本,因为必须遍历寄存器状态才能生成溢出代码,而且总的来说可能很少有活的值,所以这可能是好的(或不是)。 当然,使用 regalloc 变得更聪明可能会或可能不会在现代芯片上获得回报,因为它们具有快速缓存和 ooo 执行......

一个更微妙的成本是编译器的可维护性......它已经相当复杂了,并且由于它是一次性的并且根本不构建IR图或使用动态内存,因此它可以抵抗分层和抽象。

@RossTate

关于 funclets / gotos,前几天我浏览了 funclet 规范,乍一看,它看起来不像一次性编译器应该有任何真正的问题,当然不是简单的 regalloc 方案。 但即使有更好的方案,它也可能没问题:到达连接点的第一条边将决定寄存器分配是什么,而其他边必须符合。

@conrad-watt 正如你刚刚在 CG 会议上提到的,我想我们会非常有兴趣了解你的多循环的外观细节。

@aardappel是的,生活很快就来了,但我应该在下次会议上这样做。只是为了强调这个想法不是我的,因为@rossberg最初是为了响应

一个可能具有指导意义的参考文献有点过时,但概括了熟悉的循环概念,以使用DJ 图处理不可约的循环。

我们在 CG 中就这个问题进行了几次讨论,我写了一份总结和后续文件。 由于篇幅较长,我将其作为单独的要点。

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

我认为两个直接可行的问题(有关更多详细信息,请参阅后续部分)是:

  • 我们能否找到目前正在遭受痛苦并且将从multiloop受益的“狂野”程序? 这些可能是 LLVM 转换引入不可约控制流的程序,即使在源程序中不存在。
  • 是否存在multiloop在生产者端实现的世界,并带有一些用于“Web”Wasm 的链接/翻译部署层?

关于我在后续文档中讨论的异常处理问题的后果,可能还有更自由的讨论,当然,如果我们推进任何具体的事情,关于语义细节的标准自行车脱落。

因为这些讨论可能会有些分支,所以将其中一些讨论转入funclets存储库中的问题可能是合适的。

我很高兴看到在这个问题上取得进展。 向所有参与其中的人表示巨大的“谢谢”!

我们能否找到当前正在遭受痛苦并且会从多循环中受益的“狂野”程序? 这些可能是 LLVM 转换引入不可约控制流的程序,即使在源程序中不存在。

我想对循环推理提出一点警告:由于这个原因,当前性能不佳的程序不太可能“在野外”发生。

我认为大多数 Go 程序应该受益匪浅。 Go 编译器要么需要 WebAssembly 协程,要么需要multiloop才能生成支持 Go 协程的高效代码。

预编译的正则表达式匹配器,以及其他预编译的状态机,通常会导致不可约的控制流。 很难说接口类型的“融合”算法是否会导致不可约化的控制流。

  • 同意这个讨论应该转移到 funclets(或新的)repo 上的问题。
  • 同意如果没有 LLVM(以及 Go 和其他)实际发出最佳控制流(这可能是不可约的),很难量化找到从中受益的程序。 FixIrreducibleControlFlow和朋友造成的低效率可能是大型二进制文件中的“千刀万剐”问题。
  • 虽然我会欢迎仅使用工具的实现作为本次讨论的绝对最小进展,但它仍然不是最佳的,因为生产者现在为了方便而不得不使用此功能(但随后面临不可预测的性能回归/悬崖),或者努力将他们的输出与标准 wasm 争吵,以使事情变得可预测。
  • 如果确定“gotos”充其量只是一个工具功能,我认为您可能会使用比多循环更简单的功能,因为您所关心的只是生产者的便利性。 至少, goto <function_byte_offset>将是唯一需要插入常规 Wasm 函数体以允许 WABT 或 Binaryen 将其转换为合法 Wasm 的东西。 如果引擎需要快速验证多循环,则类型签名之类的东西很有用,但如果它是一个方便的工具,不妨让它最大程度地方便发射。

同意如果没有 LLVM(以及 Go 和其他)实际发出最佳控制流(这可能是不可约的),很难量化找到从中受益的程序。

我同意对修改后的工具链 + 虚拟机进行测试是最佳的。 但是我们可以将当前的 wasm 构建与具有最佳控制流的本地构建进行比较。 Not So Fast和其他人以各种方式(性能计数器、直接调查)对此进行了研究,并没有发现不可简化的控制流是一个重要因素。

更具体地说,他们没有发现它是 C/C++ 的重要因素。 这可能更多地与 C/C++ 有关,而不是与不可约控制流的性能有关。 (老实说,我不知道。)听起来@neelance有理由相信 Go 的情况并非如此。

我的感觉是这个问题有多个方面,值得通过多个方向来解决它。

首先,听起来 WebAssembly 的可生成性存在一个普遍问题。 这在很大程度上是由 WebAssembly 的约束造成的,即拥有一个紧凑的二进制文件,具有高效的类型检查和流式编译。 我们可以通过开发一个标准化的“pre”-WebAssembly 至少部分解决这个问题,它更容易生成,但可以保证可翻译为“真正的”WebAssembly,理想情况下只需复制代码和插入“可擦除”指令/注释,至少有一些提供这种翻译的工具。

其次,我们可以考虑“pre”-WebAssembly 的哪些特性值得直接纳入“真正的”WebAssembly。 我们可以以知情的方式做到这一点,因为我们将拥有“pre”-WebAssembly 模块,我们可以它们被扭曲为“真正的”WebAssembly 模块

几年前,我尝试将用于动态语言(https://github.com/ciao-lang/ciao)的特定字节码模拟器编译为 webassembly,但性能远非最佳(有时比原生版本慢 10 倍)。 主执行循环包含一个大型字节码调度开关,引擎经过数十年的微调以在实际硬件上运行,我们大量使用标签和 goto。 我想知道这种软件是否会受益于对不可约控制流的支持,或者问题是否是另一个问题。 我没有时间做进一步的调查,但如果已知情况有所改善,我很乐意再试一次。 当然,我知道将其他语言 VM 编译为 wasm 并不是主要用例,但我很想知道这是否最终可行,特别是因为在任何地方都能高效运行的通用二进制文件是承诺的优势之一瓦斯姆。 (感谢并道歉,如果这个特定的主题已经在其他问题中讨论过)

@jfmc我的理解是,如果该程序是现实的(即不是为了病态而设计的)并且您关心它的性能,那么它是一个完全有效的用例。 WebAssembly 旨在成为一个良好的通用目标。 因此,我认为了解为什么您会看到如此显着的放缓会很棒。 如果这恰好是由于对控制流的限制,那么在本次讨论中了解这一点将非常有用。 如果它碰巧是由于其他原因,那么了解如何总体上改进 WebAssembly 仍然很有用。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

dpw picture dpw  ·  3评论

chicoxyzzy picture chicoxyzzy  ·  5评论

artem-v-shamsutdinov picture artem-v-shamsutdinov  ·  6评论

JimmyVV picture JimmyVV  ·  4评论

arunetm picture arunetm  ·  7评论