Design: WebAssembly.Instance 可以重新编译一个已经编译好的模块吗?

创建于 2016-10-27  ·  94评论  ·  资料来源: WebAssembly/design

说我有这个代码:

let descriptor = /* ... */;
let memories = Array.apply(null, {length: 1024}).map(() => new WebAssembly.Memory(descriptor));
let instance = fetch('foo.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => WebAssembly.compile(buffer))
  .then(module => new WebAssembly.Instance(module, { memory: memories[1023] }));

是否允许WebAssembly.Instance阻塞很长时间。 例如,它可以重新编译WebAssembly.Module吗?

在大多数情况下,我会说不,但是如果已经编译的代码不是特别喜欢它收到的内存怎么办? 说,因为该内存是慢速模式内存,并且代码是在假设快速模式下编译的? 也许memories[0]是一种快速模式内存,但memories[1023]肯定不会。

这段代码呢?

let instances = [0,1,2,3,4,5,6,7].map(v => fetch(`foo${v}.wasm`)
  .then(response => response.arrayBuffer())
  .then(buffer => WebAssembly.compile(buffer))
  .then(module => new WebAssembly.Instance(module)));

是否允许对WebAssembly.Instance调用导致重新编译?

假设以上是有道理的,这里有几个相关的问题:

  • 我们是否想要一个可以编译 _and_ 实例化的返回 promise 的异步函数? 我并不是说我们应该放弃我们已经拥有的任何同步和异步 API,我提出了一个新的异步 API。
  • 浏览器如何快速公开WebAssembly.Module中编译的代码,并且WebAssembly.Memory实例适合如此快速的代码? 现在的答案似乎是“尝试一下,看看您是否能注意到”。
  • 用户如何在获得慢代码之前知道他们允许有多少WebAssembly.Memory实例(计算隐式实例,例如第二个示例创建的实例)?
JS embedding

最有用的评论

@kgryte我应该澄清一下,我的评论主要与作为执行上下文的浏览器有关。 我们已经找到了仍然公开同步 API 的 API 表面。 浏览器可能会对传递给同步 API 的模块施加大小限制(例如,Chrome 已经这样做了),但该限制可由嵌入器配置并且不需要应用于 Node.js。

所有94条评论

WebAssembly.Instance有时会导致重新编译会很好,这样不可变的全局变量可以在生成的代码中保持不变。 例如,Emscripten 通过偏移所有指向静态数据的指针来生成可重定位代码。 当模块被实例化时,偏移量作为一个不可变的全局变量传入。 如果WebAssembly.Instance可以重新编译,它可以特化生成的代码。

规范没有定义“编译”是什么,也不会让
这样做是有意义的,因为实施方法可能大不相同
(包括口译员)。 所以它不能对此有任何规范性的说法
无论哪种方式。 我们能做的最好的事情就是添加一个注释
WebAssembly.Instance预计会“快”。

2016年10月在03:24 27日,迈克尔·Bebenita [email protected]
写道:

WebAssembly.Instance 有时会导致重新编译会很好,
这样不可变的全局变量可以在生成的常量中折叠
代码。 例如,Emscripten 通过偏移所有生成可重定位代码
指向静态数据的指针。 偏移量作为不可变的全局变量传入
当模块被实例化时。 如果 WebAssembly.Instance 可以重新编译,
它可以专门化生成的代码。


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

同意这最多是一个非规范性说明。

在 SM 中,我们目前还打算让实例化永远不会重新编译,以便为开发人员提供一个可预测的编译成本模型(特别是,这样开发人员可以使用WebAssembly.compile和 IDB 来控制他们何时进行编译) . 从同步Instance构造函数中进行实例化时重新编译肯定会破坏该成本模型并可能导致重大卡顿。

但我确实很欣赏单独编译从根本上与人们可能想要做的将生成的代码专门用于环境参数的各种优化不一致。 将编译和实例化融合到一个异步操作中是有道理的,也是我们过去考虑过的事情。 当然,缺点是这会抑制显式缓存(没有Module ),因此开发人员必须做出令人不快的权衡。 一些选项:

  • impl 可以执行隐式内容寻址缓存(可以在键中包含环境参数),就像我们在 FF 中使用 asm.js 所做的那样。 这将是一种痛苦,并且具有任何隐式缓存的所有可预测性/启发式问题。
  • 我们可以创建一种新方式(例如,一个新的WebAssembly.Cache API,您可以在其中传递字节码和实例化参数并返回Promise<Instance>

后者引起了我的兴趣,并且可以提供比使用 IDB 更好的开发人员体验,也许还有机会进一步优化缓存(因为缓存是专门用于目的的),但它肯定是一个很大的功能,我们想要花一些时间考虑。

@rossberg-chromium 我似乎没有很好地解释我的目的:我不想对规范所说的内容争论不休。 我试图指出隐藏在 API 下的对于开发人员来说似乎是一个严重的惊喜。 开发人员不会期望重新编译.compile的结果。 这对我来说似乎是一个设计缺陷。

@lukewagner即使使用隐式或显式缓存,我们也可能遇到同样的问题:在同一地址空间/源中可以创建多少WebAssembly.Memory是浏览器限制。 我喜欢你的建议,但我认为它与问题无关。 如果我误解了您的建议,请告诉我。

也许.compileModule可以被赋予Memory ,并且Instance有一个.memory属性,可以传递给其他编译/实例化?

我不是要消除重新编译的可能性,我认为我们更喜欢通用的惯用 API 用法,它在第一次编译时(或在缓存检索时)具有完美的信息 wrt Memory所以如果需要,编译是否发出边界检查。

@jfbastien通过提供特定实例化参数的隐式/显式缓存(所以Memory ),我不知道如何需要重新编译。

@jfbastien通过提供特定实例化参数的隐式/显式缓存(所以Memory ),我不知道如何需要重新编译。

可能有:

  1. 创建许多Memory s。
  2. 编译代码,使用显式(慢)边界检查,因为Memory ie 太多。
  3. 缓存该代码。
  4. 离开页面。
  5. 再次加载页面。
  6. 只分配一个Memory ,它获得快速版本。
  7. 从缓存中获取。
  8. 接收慢代码Instance

在这一点上,我同意您不需要重新编译,但是我们在不需要时进行慢速边界检查有点傻。

正如我所说:我喜欢你提出的这个Cache API,我认为它使 WebAssembly 更有用,但我认为问题仍然存在。 😢

嗯,这就是我关于拥有接受实例化参数和字节码的增强缓存的观点:如果缓存的内容与实例化参数不匹配,则缓存可以自由重新编译。 所以步骤就是:

  1. 创建许多Memory s
  2. 从缓存中请求Instance ,传递其中一个(慢) Memory s
  3. 慢代码被编译、缓存并作为Instance
  4. 离开页面
  5. 再次加载页面
  6. 只分配一个Memory
  7. 从缓存中请求Instance ,传递快速Memory
  8. 快速代码被编译、缓存并作为Instance

在第 8 步之后,所有未来的页面加载都将缓存快速或慢速代码。

@lukewagner首先,您提出的缓解措施与 WebAssembly 提供确定性性能的既定目标背道而驰。 慢速和快速之间的差异最后被引用为大约 20%,所以如果一个苦心瞄准确定性性能的规范由于 API 怪癖而将其丢弃在地板上,那真的很糟糕。 我不认为具有内容寻址缓存的浏览器是正确的解决方案,因为规范已经在其他地方遇到了很多麻烦,以消除对配置文件重新编译缓存优化的需要。 例如,我们精确地承诺编译,以便即使代码没有缓存,应用程序也可以获得合理的行为。 如果规范的方式要求我们所有人都实施缓存或其他缓解措施,那么我们将无法为人们提供合理的可移植成本模型的目标。

对我来说,问题就是这样:在当前规范中,出于竞争原因,我们都必须有效地进行的优化之一(4GB 虚拟内存边界检查,我将其称为 4GB hack)无法在不牺牲的情况下完成这些事情之一:

  • 如果你总是为任何 wasm 内存分配 4GB 的虚拟内存,你就可以摆脱它。 这将阻止人们将 WebAssembly 用于小模块,因为如果您分配许多这些,您将遇到虚拟内存分配限制、虚拟内存碎片或其他问题。 我还担心,如果你允许分配很多,那么你会降低像 ASLR 这样的安全缓解措施的功效。 请注意,现有的 API 不存在这种危险,因为它们提交了它们分配的内存,并且在让您分配比物理内存允许的多得多的内存之前,它们要么 OOM 要么崩溃。
  • 如果在实例化过程中发现不匹配时允许重新编译(编译代码需要 4GB hack 但内存没有虚拟内存分配),则可以避免使用它。 如果实例化将内存移动到 4GB 区域,您也可以摆脱它,但请参阅上一点。 因此,可能任何时候发生这种情况,对于遇到它的浏览器来说,它都会是 P1 错误。

我认为这意味着规范将鼓励供应商在分配 wasm 内存时收敛到只允许 4GB 保留,或者使用缓存/延迟编译/配置文件优化来检测这一点。

最后,我不明白将任何这些非规范化的意义。 这可以是规范的,因为我们可以使 API 排除浏览器在不知道它将拥有什么样的内存的情况下编译某些东西的可能性。 我想有很多方法可以做到这一点。 例如,实例化可以返回一个承诺,我们可以删除单独的编译步骤。 这将清楚表明实例化是可能需要一段时间的步骤,这强烈暗示客户端这是进行编译的步骤。 在这样的 API 中,编译器总是知道它正在编译的内存是否有 4GB hack。

很遗憾我们现在才注意到这一点,但我很惊讶你们没有看到这是一个更大的问题。 除了我忽略的缓存之外,还有其他缓解措施吗?

@jfbastien在您的激励方案中,您指出该模块的编写是为了更喜欢快速内存。 我假设您主要是在特定模块需要时启用快速内存​​优化,并且在模块不需要时不这样做可能没问题(在这种情况下,机会主义地绊倒它也没什么不好) ,只是试图梳理优先事项)。

如果是这样,这些缓存或异步实例化的替代方法会如何:

  1. 模块作者必须要求 4GB 作为最小/最大内存
  2. 编译的变体(至少是异步的,也可能是同步的),它生成一个仅接受快速内存的实例。

对于“4GB hack”的问题以及使用它的内存和期望它的代码之间的不匹配,编译内部发出两个版本的代码是否有意义? (显然这会使用更多内存,这很可悲,但希望编译时间不会更糟,作者可以同时生成两者?)

@mtrofin如果您不打算使用它,我认为要求 4GiB 没有意义。 虚拟分配与使用意图是分开的,所以我认为我们需要将两者分开。

关于 2.:它对开发人员仍然不是很有帮助:如果他们使用该变体并且失败了,那怎么办?

@kripken我不认为双重编译是个好主意。

@kripken我认为这就是我们在没有任何其他解决方案的情况下会做的事情。

我希望 WebAssembly 在随意浏览的情况下表现出色:你告诉我一个很酷的东西,给我发送 URL,我点击它,然后我自娱自乐几分钟。 这就是让网络很酷的原因。 但这意味着许多编译将包含未缓存的代码,因此编译时间将在用户的电池寿命中发挥重要作用。 所以,双重编译让我很伤心。

@mtrofin

模块作者必须要求 4GB 作为最小/最大内存

这不太实用,因为许多设备没有 4GB 的物理内存。 此外,这很难规范。

编译的变体(至少是异步的,也可能是同步的),它生成一个仅接受快速内存的实例。

我不认为我们想要双重编译。

@pizlonator到目前为止,我们还没有考虑过需要不同代码生成模式的设计:我们总是在 64 位上分配 4gb 区域,并观察到这在 Linux、OSX 和 Windows 上的数千个内存中成功。 我们有一个保守的上限来防止可用地址空间的琐碎完全耗尽,我预计这将足以支持多小库用例。 所以我认为我们在这里解决的新限制是 iOS 有一些虚拟地址空间限制,这可能会减少 4gb 分配的数量。

因此,一个观察结果是,只需在 wasm 内存的末尾设置一个小的保护区域,就可以避免 4gb hack 允许的大部分边界检查消除。 我们最初的实验表明,基本分析(与循环无关,只是消除对具有相同基指针的加载/存储的检查)已经可以消除大约一半的边界检查。 也许这会变得更好。 因此,4GB hack 将是一个更温和、更不必要的加速。

我之前的另一个想法是使用边界检查(使用基于保护页面的消除)悲观地编译代码,然后在使用快速模式内存实例化时将它们排除。 综合起来,与理想化的快速模式代码相比,开销可能很小。

@卢克瓦格纳

到目前为止,我们还没有考虑需要不同代码生成模式的设计:我们总是在 64 位上分配 4gb 区域,并观察到这在 Linux、OSX 和 Windows 上的数千个内存中成功。 我们有一个保守的总数来防止可用地址空间的微不足道的完全耗尽,我预计这将足以支持多小库用例。 所以我认为我们在这里解决的新限制是 iOS 有一些虚拟地址空间限制,这可能会减少 4gb 分配的数量。

这不是特定于 iOS 的问题。 问题是,如果您允许大量此类分配,则会带来安全风险,因为每次此类分配都会降低 ASLR 的功效。 所以,我认为 VM 应该可以选择为它分配的 4GB 空间的数量设置一个非常低的限制,但这意味着回退路径不应该太昂贵(即它不应该需要重新编译)。

您对分配的 4GB 内存数量有什么限制? 当你达到这个限制时你会怎么做 - 完全放弃,或者在实例化时重新编译?

因此,一个观察结果是,只需在 wasm 内存的末尾设置一个小的保护区域,就可以避免 4gb hack 允许的大部分边界检查消除。 我们最初的实验表明,基本分析(与循环无关,只是消除对具有相同基指针的加载/存储的检查)已经可以消除大约一半的边界检查。 也许这会变得更好。 因此,4GB hack 将是一个更温和、更不必要的加速。

我同意分析允许我们消除更多检查,但是如果您想要峰值性能,4GB hack 是可行的方法。 每个人都想要峰值性能,我认为能够在不引起安全问题、资源问题和意外重新编译的情况下获得峰值性能会很棒。

我之前的另一个想法是使用边界检查(使用基于保护页面的消除)悲观地编译代码,然后在使用快速模式内存实例化时将它们排除。 综合起来,与理想化的快速模式代码相比,开销可能很小。

具有边界检查的代码最好为内存大小固定一个寄存器,并为内存基数固定一个寄存器。

使用 4GB hack 的代码只需要为内存库固定一个寄存器。

所以,这不是一个很好的解决方案。

除了必须纠结规范和实现的烦恼之外,将编译和实例化结合到一个承诺的操作中还有什么缺点?

问题是,如果你允许大量这样的分配,那么它就会带来安全风险,因为每一个这样的分配
分配降低了 ASLR 的功效。

我不是 ASLR 的专家,但是,iiuc,即使我们没有保守的界限(也就是说,如果我们允许您继续分配直到mmap失败,因为内核达到了它的数量- address-ranges max),只消耗整个 47 位可寻址空间的一小部分,因此代码放置将继续在这个 47 位空间上高度随机。 IIUC,ASLR 代码放置也不是完全随机的; 刚好足以让人难以预测任何事情会发生在哪里。

您对分配的 4GB 内存数量有什么限制? 你做什么工作
当您达到此限制时 - 完全放弃,还是在实例化时重新编译?

好吧,因为它是从 asm.js 天开始的,只有 1000。然后内存分配就抛出了。 也许我们需要解决这个问题,但即使有许多超级模块化的应用程序(每个应用程序有许多单独的 wasm 模块)共享相同的过程,我也无法想象我们需要更多。 我认为Memory与普通的旧ArrayBuffer ,因为应用程序自然不会想要创建数千个。

除了不得不争论规范和实现的烦恼之外,还有什么缺点
将编译和实例化合并为一个承诺的操作?

正如我上面提到的,添加一个Promise<Instance> eval(bytecode, importObj) API 很好,但现在它让开发人员处于困境,因为现在他们必须在某些平台上的性能提升与能够缓存他们编译的代码之间做出选择所有平台。 似乎我们需要一个与缓存集成的解决方案,这就是我在上面用显式Cache API 集思广益的解决方案。

新想法:如果我们添加一个异步版本的new Instance ,比如WebAssembly.instantiate并且像WebAssembly.compile ,我们说每个人都应该使用异步版本怎么办? 这是我一直在考虑的事情_无论如何_,因为如果使用补丁,实例化可能需要几毫秒。 然后我们在规范中说引擎可以在compileinstantiate完成昂贵的工作(或者两者都不做,如果引擎进行延迟验证/编译!)。

这仍然留下了当compile d Module存储在 IDB 中时该怎么做的问题,但是当有多种代码生成模式_无论如何_时,这只是一个难题。 一种想法是,存储到 IDB 或从 IDB 检索的Module保留其 IDB 条目的句柄,并将新的编译代码添加到该条目。 这样,IDB 条目将懒惰地积累其模块的一个或多个编译版本,并能够提供实例化期间所需的任何版本。

IDB 部分需要做更多的工作,但这似乎非常接近理想的性能。 WDYT?

我认为添加 async instantiate是有道理的,但我也会向compile添加一个Memory参数。 如果将不同的内存传递给instantiate那么你可以重新编译,否则你已经在编译时“绑定”了内存。

我还没有考虑到缓存足以有一个完整的意见。

@卢克瓦格纳

我不是 ASLR 方面的专家,但是,iiuc,即使我们没有保守的界限(也就是说,如果我们允许您继续分配直到 mmap 失败,因为内核达到其地址范围的数量最大值) ,将只消耗整个 47 位可寻址空间的一小部分,因此代码放置将继续在这个 47 位空间上高度随机。 IIUC,ASLR 代码放置也不是完全随机的; 刚好足以让人难以预测任何事情会发生在哪里。

ASLR 影响代码和数据。 关键是让攻击者在不追逐指向数据结构的情况下狡猾地进入数据结构的成本更高。 如果攻击者可以耗尽内存,他肯定有更多的筹码。

好吧,因为它是从 asm.js 天开始的,只有 1000。然后内存分配就抛出了。 也许我们需要解决这个问题,但即使有许多超级模块化的应用程序(每个应用程序有许多单独的 wasm 模块)共享相同的过程,我也无法想象我们需要更多。 我认为 Memory 与普通的旧 ArrayBuffers 不同,因为应用程序自然不会想要创建数千个。

1000 似乎是一个合理的限制。 我会问周围的安全人员。

正如我上面提到的,添加一个 Promiseeval(bytecode, importObj) API 很好,但现在它让开发人员陷入困境,因为现在他们必须在某些平台上的性能提升与能够在所有平台上缓存他们的编译代码之间做出选择。 似乎我们需要一个与缓存集成的解决方案,这就是我在上面使用显式缓存 API 集思广益的解决方案。

对。 我可以看到一些可以使这种 API 工作的方法。 一个俗气但实用的 API 是重载 eval:

  1. instancePromise = eval(字节码,importObj)
  2. instancePromise = eval(module, importObj)

然后 Instance 有一个 getter:

模块 = 实例.模块

其中模块是结构可克隆的。

你觉得这怎么样?

新想法:如果我们添加一个异步版本的 new Instance,比如 WebAssembly.instantiate,并且像 WebAssembly.compile 一样,我们说每个人都应该使用异步版本怎么办? 这是我一直在考虑的事情,因为如果使用补丁,实例化可能需要几毫秒。 然后我们在规范中说引擎可以在编译或实例化中完成昂贵的工作(或者两者都不做,如果引擎进行延迟验证/编译!)。

这仍然留下了当编译的模块存储在 IDB 中时该怎么做的问题,但是当存在多种代码生成模式时,这只是一个难题。 一种想法是,存储到 IDB 或从 IDB 检索的模块保留其 IDB 条目的句柄,并将新的编译代码添加到该条目。 这样,IDB 条目将懒惰地积累其模块的一个或多个编译版本,并能够提供实例化期间所需的任何版本。

IDB 部分需要做更多的工作,但这似乎非常接近理想的性能。 WDYT?

耐人寻味。 相对于我上面的想法:

优点:你是一个易于理解的抽象概念,在概念上与我们现在所说的相似。
缺点:你的建议并没有像我的建议允许的那样在用户和引擎之间产生尽可能多的协同作用。

在以下三个方面,您的提案没有给用户像我的那样多的控制权:

  1. 昂贵的工作可能发生在两个地方之一,因此用户必须计划其中任何一个都是昂贵的。 如果其中之一很昂贵,我们可能会拥有表现不佳的 Web 内容,因为它已针对碰巧便宜的情况进行了调整。 我的提议有一个地方会发生昂贵的事情,从而导致实现之间的一致性更高。
  2. 对于要缓存的编译代码的所有版本,没有明确保证的路径。 另一方面,我使用通过 API 对模块进行线程化意味着 VM 可以每次构建具有更多内容的模块,同时仍然允许用户管理缓存。 所以,如果我们第一次做 4GB 那么这就是我们将缓存的内容,但是如果我们第二次做 4GB 失败,我们将能够潜在地缓存两者(如果用户在每次编译后缓存 instance.module)。
  3. 浏览器中的异常情况或其他问题有时会导致您的方案中的双重编译,因为我们会在编译步骤中编译一件事,但随后意识到我们在实例化步骤中需要另一件事。 我的版本从不需要双重编译。

所以,我更喜欢我的。 也就是说,我认为你的提议是一个进步,所以对我来说绝对听起来不错。

这个问题取决于碎片使分配快速的频率
内存(顺便说一句,您将 4GB + 支持的最大偏移量或 8GB)失败。 如果
可能远低于 1%,那么可能并非完全没有道理
那是一个OOM的情况。

在用户浏览网页并使用大量
快速连续的小 WASM 模块,大概它们并不都在
一次。 在这种情况下,保留 4GB 块的小缓存将减轻
问题。

另一种可能的策略是生成一个版本的代码
边界检查,如果快速内存可用,只需覆盖边界
用 nops 检查。 这很丑陋,但这比一个快得多
重新编译,空间少于两次编译。

2016 年 10 月 27 日星期四晚上 9:03,pizlonator [email protected]
写道:

@mtrofin https://github.com/mtrofin

模块作者必须要求 4GB 作为最小/最大内存

这不太实用,因为许多设备没有 4GB 的物理容量
记忆。 此外,这很难规范。

编译的变体(至少是异步的,也可能是同步的),它产生一个
实例只接受快速内存。

我不认为我们想要双重编译。


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

这不仅仅是 ASLR:它也是 pagetable/allocator/etc 污染。 我们都需要与我们的安全人员_以及_内核/系统人员交谈。 或者,我们可以提前向开发人员说明每个引擎对“快速” Memory施加的限制,并使其在 API 中符合习惯,因此很难错误地使用它。

我们可以使用所有这些拐杖,例如 nop 或双重编译,但为什么还要拐杖呢?

@jfbastien我不认为 PROT_NONE 内存消耗页表条目; 我认为有一个单独的数据结构保存映射,从中延迟填充页表。

@pizlonator我喜欢这个想法,我可以看到这是我们鼓励每个人在教程、工具链等中默认使用的东西。如果您可以简单地忽略Module ,它也会更简洁、更容易教授。 这也可以解决@s3ththompson对通过将最好的 API

然而,我认为我们不应该拿走WebAssembly.compileModule构造函数:我想象你有一个“代码服务器”的场景(通过 IDB + postMessage提供跨源代码缓存new Module ),我们需要保留new Instance

因此,如果同意这一点,那么这归结WebAssembly.eval您提到的两个

不过有一个调整:我认为我们不应该有module因为这将需要InstanceInstance的生命周期内保留一些内部数据(即字节码) Module通常可以在实例化后立即进行 GC。 这将建议一个数据属性(用户可以删除,尽管他们可能会忘记),或者可能是eval的第三个版本,它返回一个{instance, module}对......

将异步单步 API 作为典型单体应用程序的推荐案例作为推荐模式是有意义的。

同意@lukewagner 的观点,即新模块 + 新实例涵盖的所有同步(内联编译)情况都是有用的。
此外,带有同步实例化的后台编译(异步)服务器似乎也很有用。

添加提出的两个 eval 变体似乎是介绍这一点的好方法。

但是我不喜欢这个名字,因为它会在(安全)人们的脑海中与 js eval 混淆(它在某种程度上类似于,但不是在范围捕获方面)。
WebAssembly.instantiate 怎么样?

哈,好点, eval 确实有一点代表。 +1 到WebAssembly.instantiate

当使用异步实例化时,开发人员的指导方针是什么?

@mtrofin 默认使用WebAssembly.instantiate ,除非他们有一些特殊的代码共享/加载方案,需要独立于任何特定用途编译Module s。

@lukewagner这似乎是合理的。

哈,说得好, eval 确实有一些代表。 +1 到 WebAssembly.instantiate。

同意。

因此,如果同意这一点,那么这归结为您提到的两个 WebAssembly.eval 重载的纯粹附加提议。 是的?

听起来就是这样。

我认为我们不应该有一个模块 getter,因为这需要实例在实例的生命周期内保留一些内部数据(即字节码); 现在模块通常可以在实例化后立即进行 GC。 这将建议一个数据属性(用户可以删除,尽管他们可能会忘记),或者可能是第三个版本的 eval 返回一个 {instance, module} 对......

当然感觉像数据属性更好。 或者让 WebAssembly.instantiate 总是返回一个实例,模块对。

这是正确的:假设您WebAssembly.instantiate的目标是获得一个 fastmemory 模块变体。 您现在获得了模块,并对其进行了结构克隆。 现在,这个模块必然需要使用支持 fastmemory 的Memory -es 进行实例化。

@pizlonator是的,我可以通过多种方式在脑海中骑自行车。 我想我喜欢更好地返回这对,因为它可能会导致更少的人意外地夹带未使用的Module

@mtrofin 重新编译仍然是必要的,当您从instantiate调用中提取Module和使用新导入的instantiate时; 我认为这个 API 添加的重点是它不会是常见的情况,它只会在根本必要时才会发生(即,您有 1 个模块访问两种内存)。

这个线程越来越长,看起来它正在收敛,但要 100% 确定我们需要编写我们期望不同用户编写的代码:

  1. 单个模块的异步实例化。
  2. 模块的异步实例化,与其他模块共享内存。
  3. 单个模块的同步实例化(我认为同步多模块没有用?)。
  4. 缓存所有这些(放入缓存,以及检索和实例化,使用内存)。
  5. 更新单个.wasm模块,并缓存其他模块的加载。

还要别的吗? 听起来@lukewagner对导入有一些想法,但我并不完全理解。

这意味着此模块的后续使用必须异步实例化,否则可能会因同步实例化时间长得惊人而阻塞 UI 线程。

@jfbastien我想了解我们希望开发人员编写的每个片段,是什么促使他们走这条特定道路,以及开发人员必须拥有哪些信息才能做出决定。

@mtrofin是的,给定一个Module m ,你会调用异步的WebAssembly.instantiate(m) 。 您_可以_调用new Instance(m)并且它可能很贵,但这与new Module(m)没有什么不同。

@jfbastien假设当您说“异步实例化”时,您的意思是“异步编译和实例化”,以下是简短版本:

  1. WebAssembly.instantiate(bytecode, imports)
  2. WebAssembly.instantiate(bytecode, imports) ,其中imports包括共享内存
  3. new Instance(new Module(bytecode), imports)
  4. 在所有的情况下,你可以得到Module ,那么put ,在一个IDBObjectStore 。 稍后,您get一个Module m返回并调用WebAssembly.instantiate(m, imports)
  5. 没有什么特别在这里:你WebAssembly.instantiate从字节码一个模块和instantiate其余从Module期从IDB拉。

如果您觉得可以使用同步编译,我们是否应该推荐使用同步实例化,如果您觉得应该使用异步编译,我们应该推荐使用异步实例化吗?

除此之外,我担心开发人员现在将面临一个更复杂的系统:更多的选择会产生我们计划进行的优化,而且我不确定开发人员是否有正确的信息来进行权衡。 从开发人员的角度思考,是否有一小部分他们关心并愿意表达的担忧? 我们曾经讨论过开发人员“以精确的故障点为代价进行优化”(这是重新提升边界检查)。 另一种选择是“优化”标志吗?

@mtrofin 99% 的开发人员将编写(或由工具链为他们生成)将是WebAssembly.instantiate 。 如果您正在编写一些代码共享系统,您只会将同步 API 用于特殊的“我正在使用 wasm 编写 JIT”和WebAssembly.compile ,因此我认为“入门”教程将专门涵盖WebAssembly.instantiate

@lukewagner我注意到您在上面的 #3 new Module() 中添加了导入。 我认为将它添加到 WebAssembly.compile 是一个好主意,并完善了可能性。
这样,如果您想在编译时提示有关内存的信息,则可以。
如果您稍后使用不同的导入再次实例化,尤其是同步导入,您可能会遇到问题。

更改摘要(我很清楚):

  • 添加 WebAssembly.instantiate(bytes, import) 返回 {instance:, module:} 的承诺
  • 添加 WebAssembly.instantiate(module, import) 返回 {instance:, module:} 的承诺
  • 更改为新模块(字节,导入)返回模块
  • 更改为 WebAssembly.compile(bytes ,imports ) 返回实例的承诺

如果从编译匹配实例化导入,则在某处说明实例化将很快的期望。

WDYT?

哦,糟糕,我打算将导入作为参数放入Instance 。 我不相信Modulecompile有必要。 [编辑:因为如果你有它们,你只需拨打instantiate ]

所以这意味着对于端到端异步情况,您可以知道您将绑定到 4GB hack 内存,但不适用于 JITed 过滤器内核或后台编译项目(除非您还创建了一个 throw-离开实例)?

+1 专注于对异步编译和实例化的指导 - 使消息变得简单,并向开发人员隐藏决策问题的复杂性。

是的,我认为我们都同意我们会指出:
第一次:
WebAssembly.instantiate(bytes, import) -> {module, instance} 的承诺(缓存模块到 indexeddb)
第二次:
WebAssembly.instantiate(module, import) -> {module, instance} 的承诺

有人反对这是主要模式吗?

我对编译/新模块的导入感到厌烦。 这似乎是一个有用的提示。
不过,我愿意将其作为一种可能性提及并推迟将该 arg(它可能是可选的)添加到 Post-MVP。

想法?

@mtrofin (好吧,从技术上讲,只是instantiate 。)

@lukewagner (我认为这就是@mtrofin 的意思)

@lukewagner@flagxor好的,但我们保留了异步编译 API,对吗?

这个场景怎么样:你会得到一个像 PhotoShop 这样带有大量插件的应用程序。 每个插件都是一个 wasm 模块。 您启动主应用程序并设法分配触发 fastmemory 的神奇内存大小(对于这种情况似乎是合理的 - 一个应用程序,内存不足)。

您想并行编译许多插件,因此您解雇了一些工作人员来执行此操作。 您不能将这些编译传递给您将使用的实际内存(对吗?)。 因此,根据默认设置,您会为插件进行慢速内存编译,然后当插件连接到应用程序时,会为快速内存进行大量代价高昂的异步重新编译。

如果我们购买这个场景,那么感觉将一些内存描述符(明确地说,没有实际的后备内存)传递给编译 API 可能是有价值的。

是的,应该可以(甚至鼓励)将Memory传递给编译。

@mtrofin对, compile用于高级用途。 我认为该插件示例是一个有效的情况,您希望 _compile_, _and_ 您有Memory ,但您不想实例化(还)。

@pizlonator顺便说一句,我想早点问,假设“如果每个进程有超过 1000 个 4gb 映射,则抛出”hack 足以解决 ASLR/安全问题,是否_仍然_由于平台而需要慢速模式/快速模式虚拟地址配额限制? 因为如果没有,即使对于高级用户来说这甚至不是性能考虑因素,那当然也很好。 (当然,出于我们提到的其他原因,添加instantiate API 似乎很有用。)

还有一些应用程序可能会受益于 2 的幂加溢出区域的内存大小,其中应用程序已经屏蔽了指针以移除标记,因此可以屏蔽高位以避免边界检查。 这些应用程序需要在编译之前推理它们可以接收的内存分配大小,方法是修改用于屏蔽的全局常量,或者在解压缩为 wasm 时放入合适的常量。

还有一些运行时可能想要利用的零缓冲区优化,并且每个进程只会有一个这样的缓冲区。

如果它可以在编译应用程序之前推理所需的内存和可用内存,它也会使平台更加用户友好。 例如,允许浏览器或应用程序通知用户他们需要关闭某些选项卡才能运行应用程序,或者在不降低性能的情况下运行它。

Web 浏览器可能希望拥有一种可供用户选择的专用应用程序模式,他们可能在有限的设备上运行,并且需要他们可以获得的所有内存和性能来运行一个应用程序。 为此,它需要能够及早地对需求进行推理。

在编译之前不需要分配内存,而是进行合理的保留。 在有限的设备上,单独编译可能会使用大量内存,因此即使是大量的 VM 分配也可能是一个障碍。

这些不是新问题,多年来一直在讨论中。 需要内存资源管理,需要和代码生成协调。

@lukewagner我是这么认为的,因为如果我们限制在 1000 个模块,那么我会担心容差不够大。

  • 我会担心需要这个天花板下降的攻击浮出水面。
  • 我会担心堆栈中其他地方的优化会减少我们可用的虚拟地址空间量,这将要求我们重新评估上限是否足够低。
  • 我会担心防止故意创建 1000 个模块的编程风格。 例如,我知道大多数 JavaScriptCore 框架客户端创建一个 VM,做一点工作,然后销毁它。 如果 WebAssembly 在 JS 中的使用方式与 JSC 在 Objective-C 中的使用方式相同,那么为了使其在 64 位系统上工作,GC 必须知道,如果您分配 1000 个内存 - 即使每个都很小 - 那么您必须GC 以防下一次分配成功,因为这 1000 个内存现在无法访问。 在已经有 10 个 4GB 黑客内存之后分配非 4GB 黑客内存的能力意味着 GC 不必非常改变它的启发式。 当您在实例化->运行->死循环中分配第 1001 个模块时,它不必执行 GC。 这对使用小内存的模式是有益的。 任何低于 1MB 的东西,拥有 1000 个就开始变得有意义了。 我可以想象人们在 64KB 中做有用的事情。
  • 我担心这对其他 JavaScript 上下文不太有用。 我想为 JSC C API 和 Objective-C API 客户端敞开大门,让他们可以从他们的 JS 代码访问 WebAssembly API。 这些客户可能更喜欢我们分配的 4GB 内存数量的小限制。 在这种情况下甚至可以配置配额是很诱人的。

我喜欢改进的 API 消除了对内存数量的人为上限的需要,或者重新编译的需要,或其他不受欢迎的事情。 我不喜欢人造天花板,除非公差非常大,我认为这里不是这种情况。

@pizlonator足够公平,它是一个更干净/更简单的 API,所以我认为添加它很好。

至于为什么我不关心你提到的那些项目(此时):

  • 很可能需要提高限制; 这很容易。
  • 在任何合理的限制下,只会使用总 64 位地址空间的一小部分,所以我不知道这里的攻击向量是什么; 确定内容有多种方式OOM本身
  • 我们将 GC 启发式算法与预留大小相称,因此通过Memory搅动只会导致更积极的 GC。 更多的 GC 不是很好,但我不确定这会是一种常见的模式。

但是谁知道我们将来会看到什么,所以我认为现在内置灵活性很有用。

我认为尝试在 js API 中暴露过多的实现细节是一个坏主意(尤其是架构/平台特定的细节)。 我认为实现对快速内存有自己的内部限制就足够了。

拥有一个异步实例化函数似乎是合理的,但我认为如果我们想用它来提供编译器提示,我们应该使用其他东西。 例如,我们可以将模块扩展为具有请求优化单例、优化多个实例、优化加载时间、优化可预测性能等的标志部分。当然,引擎所做的(如果有的话)完全取决于实现,但它给了开发人员一个可以转动的旋钮,竞争将使浏览器供应商保持诚实。

2016 年 10 月 28 日星期五上午 2:15,JF Bastien通知@ github.com
写道:

是的,应该可以(甚至鼓励)将 Memory 传递给
汇编。

我认为应该劝阻而不是
完全导入/导出记忆。 毕竟如果一个模块没有导入
或者导出内存,可以在编译时预留内存。 我知道我们
希望能够有效地处理一些复杂的模块
应用程序会想要做,但我希望单一的 WASM 应用程序会
比我们预期的更常见。 也许我在这里是少数,但是
我宁愿看到较少动态绑定的模块。

您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/WebAssembly/design/issues/838#issuecomment -256805006,
或静音线程
https://github.com/notifications/unsubscribe-auth/ALnq1KXrUWaegRZwEhznmT1YcyI33IN9ks5q4T6TgaJpZM4Kh1gM
.

我完全同意你的看法,我认为单体应用将成为主要用例,至少在 mvp 之后的几年内是这样。 担心当你加载了成千上万个模块时会发生什么,这是在假设一个尚不存在的 wasm 生态系统。

那么还有什么办法可以解决这个问题呢? 一件事是更新http://webassembly.org/getting-started/js-api ,我可以这样做。 另一个是 binaryen 默认发出这个(听起来不错@kripken?)。 @titzer Canary 是否实现了WebAssembly.instantiate

还要别的吗?

@lukewagner :不确定你在问什么,这个问题的讨论很长。 你的意思是从当前的同步 WebAssembly.Instance API 更改 binaryen,它使用这里提出的新的基于承诺的 API 之一吗? 那有必要吗-我们是否要取消旧方式?

@kripken对,改用WebAssembly.instantiate 。 我们并没有删除旧的方式,但新的方式更有效并且默认使用。

我们可以在生成 HTML 时使用基于 promise 的 API,但是许多用户生成 JS 并自动添加异步步骤并非易事。 我们可以为人们记录这一点。 但我想我们应该只在所有浏览器中都这样做时才这样做。

@kripken我不确定我是否理解:当前的方法是根本不使用异步 API?

是的。 请参阅此问题以添加异步内容。

我假设@kripken Node 会有工作承诺。 甚至 shell 也有办法显式排空 promise 队列(从而同步执行所有解析); 在 SM 中它是drainJobQueue()而在 V8 中我听说有一个%RunMicroTasks() 。 似乎您可以简单地进行功能测试WebAssembly.instantiate并在默认情况下使用它。

当然,但首先,Promise 支持可能在最新的 node.js 中,但它不在常用版本中(例如 Linux 发行版中的默认版本)。 其次,更大的问题是,当我们发出 HTML 时,我们可以控制页面的加载方式(emcc 为用户发出加载代码),而当我们发出 JS 时,假定 JS 只是线性执行,而用户依赖在那,例如他们可以在那个JS之后有另一个脚本标签。 在这种情况下,用户编写自己的加载代码。

由于这两者,如前所述,我们可以在发出 HTML 时使用 Promise API(那么您肯定不在 shell 中,并且可以控制加载),但在发出 JS 时则不能。 在那里我们只能记录它。

Node 有支持 WebAssembly 但不支持 Promise 的版本吗? Node 人关心这些版本吗?

我不明白直线 JS 的东西。 如果你总是返回一个 Promise,那是不是 Just Work(代码的用户需要消费这个 Promise)?

我不知道第一个问题的答案,但是 polyfill 可能会让 wasm 在缺乏承诺的节点版本上运行。 虽然,我认为也有可能对 promise 进行 polyfill,因为 node 已经有一段时间的 setTimeout 版本了,但我也不确定。

关于直线问题:emcc 发出 JS 来设置运行时并连接到已编译的代码。 脚本标签中的一些 JS 可能会在调用编译后的代码后立即调用,例如使用ccall 。 换句话说,emcc 的 JS 输出不是承诺,所以我不确定你所说的“返回承诺”是什么意思——谁会返回它,谁会收到它? 但无论如何,如前所述,推荐的路径是让 emcc 发出 HTML,在这种情况下,我们可以创建异步加载代码。 只是有些用户更喜欢直接控制加载。 如果更好,我们需要鼓励他们使用 async wasm 的东西。

带有 WebAssembly 但没有 Promise 的 IMO 节点不是我们应该担心的设计约束。 在这种情况下,WebAssembly 的 polyfill 是非常愚蠢的。

您正在描述代码今天的作用。 我不完全理解它,但我想备份:我希望网页上的代码使用 Promise API。 Emscripten 是此类代码的生产者。 我不明白是什么阻止了它发出使用承诺的代码。 如果你说“这是一项重要的工作,因为它不是今天的工作方式,但我们会完成它”,我完全没问题。 但从我们的讨论来看,我不确定这是否是您所说的。

您指出的问题仅仅是关于使用异步 API 进行缓存吗? 我猜这是一个开始,但我想达到的最终状态是即使在第一次加载时也使用异步 API。

为什么在节点上使用 polyfill 是愚蠢的? 在这种情况下似乎仍然有用,即使比其他情况少:)

再说一遍:Emscripten 在发出 HTML 时使用 promise API。 这就是推荐的路径。 所以你的问题的答案是“是”。 这不是重要的工作。 它在那个问题中被勾勒出来,是的,专注于缓存,但我从(旧的)离线讨论中添加了注释,在这样做的同时,我们还应该在那里做一些其他的异步优化,因为我们可以并且它微不足道。

这是否解决了您的担忧?

我要说的是,当 Emscripten 发出 JS(不太推荐的路径)时,关于该输出的保证与在内部执行一些异步魔术的代码不一致。 我们会破坏人们的代码,这是我们不想做的。 正如我所说,有人可以编写在假定它已准备就绪的 JS 之后立即同步运行的 JS。 所以在这种情况下我们将无法使用承诺。 想象一下:

````

````

doSomething需要Module.my_func存在。 如果output.js刚刚返回了一个承诺,那么它还不存在。 所以这将是一个突破性的变化。

这现在有意义吗?

为什么在节点上使用 polyfill 是愚蠢的? 在这种情况下似乎仍然有用,即使比其他情况少:)

wasm 的 polyfill 并不傻。 迎合没有 wasm、polyfill 和没有承诺但不 polyfill 的 Node 安装是愚蠢的。 这是半途而废。 他们应该得到另一半的屁股😁

再说一遍:Emscripten 在发出 HTML 时将使用 promise API。 这就是推荐的路径。 所以你的问题的答案是“是”。 这不是重要的工作。 它在那个问题中被勾勒出来,是的,专注于缓存,但我从(旧的)离线讨论中添加了注释,在这样做的同时,我们还应该在那里做一些其他的异步优化,因为我们可以并且它微不足道。

这是否解决了您的担忧?

好的,那很好! 只要大多数网页使用promise API,我就很高兴。

[剪辑]

这现在有意义吗?

是的。 谢谢解释。

不过,从我的 POV 来看,Emscripten 不仅仅从事发布 JS 的业务! 你的例子对 Ye Olden Codes 有意义,但新的东西应该假设承诺 IMO。

顺便说一句,我查看了切换 webassembly.org/demo 以使用instantiate并且它有点棘手 b/c 当前同步new Instance出现在需要同步结果的上下文中。 因此,一旦我们将 Binaryen 更新为默认发出instantiate ,从头开始重建 AngryBots 演示会很好。

是的,但请注意,从头开始重建可能还不够 - 我相信 Unity 使用自己的加载和 HTML 代码。 因此,我们需要像前面提到的那样记录和传达这个问题,以便他们可以做必要的事情(或者我们可以让他们让 emcc 发出 html,但我不知道这对他们是否可行)。

是的,但请注意,从头开始重建可能还不够 - 我相信 Unity 使用自己的加载和 HTML 代码。 因此,我们需要像前面提到的那样记录和传达这个问题,以便他们可以做必要的事情(或者我们可以让他们让 emcc 发出 html,但我不知道这对他们是否可行)。

鉴于使用WebAssembly.instantiate API 的潜在缺点,我认为值得让他们考虑使用它。

是否记录了这些缺点? 在主要 wasm 网站或emscripten wasm wiki 页面上提供明确的指导将是一个方便的指向人们的事情。

我只是自己浏览了这个很长的问题,我还不清楚缺点,所以我也想读一读:)

@kripken当前代码的挑战在于 binaryen/emscripten 仅在同步需要导出的同时提供必要的导入(需要作为instantiate )。 如果导入可以“预先”可用,那么在异步 XHR 的尾部添加WebAssembly.instantiate就很简单了(就像我在当前演示中使用异步compile所做的那样) 看到的,我们目前的AngryBots WASM版本是不理想的,并且需要反正劲达。

哦,我不明白在 wasm XHR 开始之前就可以使用导入是这里的关键。 那就麻烦多了。 所以我之前说的大部分都是错误的。

为了让我们拥有导入,我们需要下载、解析并运行所有的 JS 胶水。 如果我们在 wasm XHR 开始之前完成所有这些工作,那么这将是一个与我们现在截然不同的加载方案和一系列权衡。 特别是,对于中小型项目,也许这甚至不是加速,我想我们必须对此进行衡量 - 如果我们还没有?

对于 Unity 来说,这不是一件简单的事情。 这将需要对 emcc 发出的代码进行重大更改,以便可以在编译代码准备好之前运行 JS 胶水。

也许我们想要考虑一种新的 JS 发射模型,一个 JS 文件用于导入,一个 JS 文件用于其余部分? 它将是选择加入的,所以我们不会破坏任何人。 无论如何,有很多考虑,如果没有测量,很难猜测什么是最佳的。

@kripken不是在 XHR 之前而是在它完成之后,并且在我们开始运行想要同步访问导出对象的脚本之前的一段时间。 我希望它可以像将使用导出的代码放在实例化解析时调用的某个回调中一样简单。

嗯,我想我仍然没有完全理解这一点,抱歉(特别是,我不明白为什么异步会与获取导入交互 - 让导入不同步工作的好处是什么?)。 但似乎我上面提到的问题仍然相关,即使这是在 XHR 完成之后 - 也就是说,我们现在有一个脚本来生成导入并接收导出。 将其拆分可能会导致权衡 - 我们只需要在有时间时进行衡量。

关于将使用导出的代码放在回调中,遗憾的是它不会仅仅因为前面提到的原因而起作用,但我们可以作为这些测量的一部分进行调查,添加一些新的编译模式。

instantiate的签名是WebAssembly.instantiate(bytes, importObj) ,因此要触发异步编译+实例化,您需要传递importObj

离线交谈时, @lukewagner给我留下了深刻的印象,这里的主要问题是在编译模块时拥有内存。 总的来说,似乎有三个问题与在实践中使用这个新 API 的难易程度有关:

  1. 编译时提供内存。
  2. 编译时提供所有导入(前一个的超集)。
  3. 异步执行所有这些操作。

鉴于工具链目前的工作方式,如上所述,做 2 + 3 很难,因为它会破坏现有用户。 我们可以做到,但可能不希望在默认情况下启用它,至少最初不是 - 我们需要咨询社区成员等。并且要真正做好 - 没有新的开销 - 需要时间和工作(快速完成可以通过在导入或导出上添加一个间接层来完成)。

但是其他一些选项非常容易使用:

  • 1 + 3 需要像instantiate(bytes, Memory) -> Promise这样的新 API。 这很容易使用的原因是无论如何都可以尽早创建 Memory(而几乎所有其他导入都是 JS 函数,我们不能在早期创建)。
  • 2 本身,没有 3,即new Instance(bytes, imports) 。 也就是说,二进制数据的同步编译 + 导入。 这很容易使用的原因是我们当前的代码是这样做的: instance = new WebAssembly.Instance(new WebAssembly.Module(getBinary()), imports)所以我们只需将它折叠成 1 个 API 调用。

我认为最后一个选项是有道理的。 它基本上意味着添加新 API 的同步版本,使事情与我们现有的同步编译选项更加对称。 而且我看不出有理由将在编译时了解内存的新优化与异步编译联系起来吗? (异步很棒,但一个单独的问题?)

我建议描述导入(内存)的元对象来打破在编译之前需要分配导入的约束。 如果提供的内存与预期不匹配并且不会延迟重新编译,则instantiate方法可能会抛出错误。

能够在内存受限设备上分配线性内存之前进行编译也非常有用,还能够优化在线性地址空间中分配为零的内存的编译,以及可能具有保护区的内存,以及针对具有固定最大大小的内存进行优化,该大小可以是 2 的幂加溢出区。 这个元对象也可以被 wasm 用户解码器/重写器用来发出针对协商内存特性优化的代码。

这是许多年前在这个项目开始时被建议为必要的东西!

@kripken虽然我认为同步 API 在技术上是必要的,但它们绝对应该是不推荐的路径,否则我们将引入大量不必要的主线程卡顿,与 asm.js 相比是一种回归 +

我一直想提供一个更好的例子来说明为什么我们需要明确地阻止(或者,正如我过去所争论的那样,甚至不允许)同步编译/实例化。 希望这为讨论提供了额外的素材。

让我们缩小一下,谈谈用户体验。


这是异步加载AngryBots 演示(通过 asm.js)、左侧和同步(通过 V8 中的 WebAssembly)之间的比较,右侧。

comparison

同步编译提供了糟糕的用户体验并破坏了进度条或任何其他应用程序加载的视觉指示。

不管付出什么努力,我认为我们需要共同努力,确保 Emscripten、Unity 和其他工具默认导出异步加载 WebAssembly。 cc @jechter @juj不仅如此,我认为我们需要让开发人员难以陷入编写同步加载代码的陷阱。

我们需要明确鼓励WebAssembly.compileWebAssembly.instantiate并阻止new WebAssembly.Modulenew WebAssembly.Instance


让我们深入一点,看看右侧的进度条对于 WebAssembly 来说有多糟糕。

这是使用 DevTools 性能面板捕获的跟踪:

screen shot 2016-12-28 at 1 26 59 pm

我的 MacBook Air 需要 30 秒才能显示任何进度条。

什么需要那个时间? 当主线程完全锁定时,让我们放大到 wasm 模块下载后的大约 20 秒:

screen shot 2016-12-28 at 2 18 36 pm

编译耗时约 20 秒,实例化耗时约 2 秒。

请注意,在这种情况下,Unity 加载器已经在调用异步WebAssembly.compile如果支持),所以 20 年代是因为 V8 仍然在后台进行同步编译。 cc @titzer @flagxor这是 V8 真正需要解决的问题。

2s 实例化卡顿是由于 Unity 加载器代码调用同步new WebAssembly.Instance 。 这是我们真正需要在 Unity 代码和 Emscripten 中解决的问题。


我认为还值得一提的是,这不是_仅仅_开发人员是否会用脚射击自己的风险。 平均网页包含数十个第三方脚本:这些脚本都有可能包含顶级文档卡顿 WebAssembly。

(如果您想更详细地探索跟踪,可以在此处查看完整的时间线:https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://www.dropbox.com/s/ noqjwql0pq6c1wy/wasm?dl=0)

我 100% 同意 async 很棒:) 但是我说它与在编译模块的同时获取内存的优化是正交的。 并且我们不能轻易地获得 async+那个优化——我们可以得到那个组合,但要么以一些开销为代价,要么以时间为代价,如果我们为它引入一个新标志并找到一种方法将它分阶段作为默认值.

因此,如果我们现在想要内存/编译优化,默认情况下,并以全效率运行,那么将其与异步联系起来是一个问题。 但是,如果它不紧急,或者默认情况下我们可以不启用它,或者我们可以接受一些新的开销,那么没问题。

由于开发人员选择使用 wasm,我认为借此机会使用新的默认值稍微更改顶级结构(如有必要,可以选择加入旧行为)是有意义的。 使用同步的人

@kripken我认为你得出的结论与我、 @ lukewagner@s3ththompson 的结论不同,因为对你来说最重要的部分是为现有开发人员提供从 asm.js 的无缝过渡。

这样对吗?

我同意这是一个有效的问题。 我认为它的权重较低,因为 IMO 与 WebAssembly 相比,我们引入了比 asm.js 更多的开发人员。 我想避免烧毁早期采用者,但是 IIUC 现有的开发人员非常灵活并且需要异步编译。

如果我们假设他们想要异步编译,并且愿意重构一些以获得它,那么剩下的就是在编译时拥有内存,这就是这个 API 提供的。 这是非常可取的,因为它避免了一个陷阱。

早些时候,您对 Emscripten 方面为支持这一点所涉及的工作量表示担忧。 你仍然认为这是一个问题吗?

FWIW,Unity 5.6 将使用 WebAssembly.instantiate。

乔纳斯

2016 年 12 月 28 日晚上 11:42,Seth Thompson [email protected]写道:

我一直想提供一个更好的例子来说明为什么我们需要明确地阻止(或者,正如我过去所争论的那样,甚至不允许)同步编译/实例化。 希望这为讨论提供了额外的素材。

让我们缩小一下,谈谈用户体验。

这是使用异步调用(通过 asm.js)、左侧和同步调用(当前的 WebAssembly 实现)加载 AngryBots 演示之间的比较,右侧。

同步编译提供了糟糕的用户体验并破坏了进度条或任何其他应用程序加载的视觉指示。

不管付出什么努力,我认为我们需要共同努力,确保 Emscripten、Unity 和其他工具默认导出异步加载 WebAssembly。 cc @jechter @juj不仅如此,我认为我们需要让开发人员难以陷入编写同步加载代码的陷阱。

我们需要明确鼓励 WebAssembly.compile 和 WebAssembly.instantiate 并阻止新的 WebAssembly.Module 和新的 WebAssembly.Instance。

让我们深入一点,看看右侧的进度条对于 WebAssembly 来说有多糟糕。

这是使用 DevTools 性能面板捕获的跟踪:

我的 MacBook Air 需要 30 秒才能显示任何进度条。

什么需要那个时间? 当主线程完全锁定时,让我们放大到 wasm 模块下载后的大约 20 秒:

编译耗时约 20 秒,实例化耗时约 2 秒。

请注意,在这种情况下,Unity 加载程序已经在调用异步 WebAssembly.compile(如果支持),所以 20 年代是因为 V8 仍然在后台进行同步编译。 cc @titzer @flagxor这是 V8 真正需要解决的问题。

2 秒的实例化卡顿是由于 Unity 加载程序代码调用同步的新 WebAssembly.Instance。 这是我们真正需要在 Unity 代码和 Emscripten 中解决的问题。

我认为还值得一提的是,这不仅仅是开发人员是否会射中自己的风险。 平均网页包含数十个第三方脚本:这些脚本都有可能包含顶级文档卡顿 WebAssembly。

(如果您想更详细地探索跟踪,可以在此处查看完整的时间线:https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://www.dropbox.com/s/ noqjwql0pq6c1wy/wasm?dl=0)


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看,或将线程静音。

@jfbastien也许我不明白你所说的“结论”是什么意思。 我主要只是在这里介绍选项。 我自己也没有结论。

如果我们现在想要编译时内存选项,我提出了一些可以让我们尽快拥有它的选项,简单地修改我们当前的同步编译。

或者,如果我们希望现在以异步方式选择,我提出了一个选项(“1 + 3”,即,在编译时不接收所有导入,只接收内存),也可以让我们尽快获得它。

或者,如果我们现在想专注于使用当前 API(而不是“1 + 3”)的该选项的异步版本,那么我们也可以得到它,只需要一些仔细的计划,因为这将是一个突破性的变化。 我不认为我们在这里决定中断现有用户是一种选择,因此我们需要咨询社区。 可能很少有顾虑,我们就可以做,在这种情况下,多少努力取决于我们愿意容忍多少开销——如果我们根本不能接受任何开销,那么它可能会很多。 或者可能会有问题,这是我个人的直觉——任何重大更改都必须逐步完成——在这种情况下,需要更长的时间,我们可能会从一个新选项开始,并最终计划将其设为默认值。

再次,我没有得出任何结论。 以上都是选项。 这实际上取决于您更关心什么:编译时内存,或异步,或两者兼而有之; 以及容忍新开销与不容忍的问题; 以及您是否迫切想要这个或可以等待等等。我很乐意在 emscripten 方面提供上述任何帮助,但人们会做出决定。

或者,如果我们现在想专注于使用当前 API(而不是“1 + 3”)的该选项的异步版本,那么我们也可以得到它,只需要一些仔细的计划,因为这将是一个突破性的变化。 我不认为我们在这里决定中断现有用户是一种选择,因此我们需要咨询社区。 可能很少有顾虑,我们就可以做,在这种情况下,多少努力取决于我们愿意容忍多少开销——如果我们根本不能接受任何开销,那么它可能会很多。 或者可能会有问题,这是我个人的直觉——任何重大更改都必须逐步完成——在这种情况下,需要更长的时间,我们可能会从一个新选项开始,并最终计划将其设为默认值。

确定我明白:

  1. 什么是间接费用?

    • 这些开销是 Async + Memory-at-compile-time 固有的,还是可以在以后修复的实现约束? IMO,如果它们是 A+M 固有的,那么我们应该尽快修复 API。

    • IIUC 开销是导入或导出的临时问题,不是 A+M 固有的,对吗? 你能再详细一点吗?

  2. 我们在谈论打破哪些用户? 想要相同代码也针对 wasm 的 asm.js 用户?

换句话说:如果我们有无限的时间并且没有“遗产”,那么 WebAssembly 的理想最终状态是什么? 我认为我们已经为理想的最终状态而设计,并且您指出了前进道路上的障碍,但我仍然不确定情况是否如此。 如果是这样,那么我不确定我是否有足够的信息来确定 WebAssembly 是否需要权宜之计来缓解过渡,或者 Emscripten 用户(以及哪些用户)的子集是否可以接受临时负担。

关于一些选项中的开销:如上所述,棘手的事情是我们必须同步发送导入和接收导出,并且在 JS 准备好之前我们没有 JS 导入(但我们确实有内存!)。 解决这个问题的一种方法是在导入或导出上添加一个间接层。 例如,我们可以很早就提供 thunk 而不是真正的导入(在 HTML 中,在 JS 之前),所以我们似乎是同步提供它们(但它只是 thunk,即使在我们拥有 JS 之前也很容易创建) . 然后当我们有了 JS 时,我们可以更新 thunk 以指向正确的位置。 这将增加另一个 JS 调用的开销和每个导入调用的 JS 对象查找。 还有一些代码大小和启动方面的开销。

关于破坏用户:我们希望现有的 emscripten 用户能够翻转标志并开始工作 wasm 。 许多用户和合作伙伴对此感到高兴。 它让他们可以比较 asm.js 和 wasm,并且让他们花费精力在移植上的项目从 wasm 中获益,而无需进行新的移植工作。 并且它避免了这样的风险:如果他们在移植到 wasm 时还需要异步化他们的启动代码,并且某些事情发生了故障,他们可能会不必要地责怪 wasm。

在 JS 准备好之前我们没有 JS 导入(但我们有内存!)

这似乎是偶然的,而不是我们应该永远使用 API 进行烘焙的东西。 此外,对于原因@ s3ththompson解释,甚至忽略了内存在编译时间的问题,我们需要实例是异步反正。 所以我同意@jfbastien 的观点,即我们在这里拥有理想的最终状态 API,这不应该是我们在这里妥协的地方。

关于破坏用户:如果我们提供一个简单的迁移路径(如“加载”事件/承诺),用户迁移应该很简单,胡萝卜是所有主要的性能胜利。 我认为我们没有任何证据表明确切的 asm.js 源代码反向兼容是默认配置的硬性要求; 我们有很多相反的例子:用户愿意做任何事情来实现最佳性能,因为这当然是重点。

@lukewagner离线交谈,我们认为最好的方法是在 emscripten 的默认推荐设置中为 wasm 启用实例化,通过添加一个间接层(如上所述,但在导出上)。 对于大多数用例,它的开销可能可以忽略不计。 因此,这为我们带来了我们所有人都想要的好处。

最终,我们可以摆脱这个小开销,但这将是一个突破性的变化,需要更多的计划和邮件列表讨论等,因为没有明显好的/正确的方法来做到这一点,我们意识到。 但由于我们认为间接层的开销很低,所以这部分并不紧急。

@kripken很棒! 有一个图表显示不同的实例/JS/导入/导出/内存/表会很有帮助。 你想把这个讨论移到别处(Emscripten / binaryen repo)吗? 我对我认为 C++ 代码的组织方式有一个清晰的认识,但现在很明显,您没有相同的图片! 你在那里有更多的经验,所以我很乐意从中学习并尽我所能提供帮助。

@jfbastien :当然。 我还不清楚你在图表中寻找什么,但是是的,也许另一个地方更好。 为了在 emscripten 中实现这一点,有一个前面提到的问题, https://github.com/kripken/emscripten/issues/4711 ,如果它没有涵盖你想要的内容,也可以随意打开另一个。

IIUC Emscripten 现在默认使用它。 关闭。

要跟进@s3ththompson评论,请注意同步编译和实例化很有用。 值得注意的是,我最近遇到了一种情况,我想在 Node.js ( v7.7.2 ) 中同步加载和编译 WebAssembly 模块。 如果只有返回承诺的 API 可用,这意味着我将无法提供同步导出。

在决定是提供异步、同步还是同时提供两种 API 时,请记住浏览器上下文并不是人们想要使用 WebAssembly 的唯一环境。 许多人,包括我自己,都对 WebAssembly 作为与 Node.js 运行时相结合的高效编译目标的潜力感兴趣,类似于 JVM。 虽然异步导入/导出可能会出现在 Node.js 中,但在可预见的未来,同步导入和导出仍将是主导模式。 在这种情况下,加载 WebAssembly 模块并同步编译和实例化该模块的能力非常有用。

@kgryte我应该澄清一下,我的评论主要与作为执行上下文的浏览器有关。 我们已经找到了仍然公开同步 API 的 API 表面。 浏览器可能会对传递给同步 API 的模块施加大小限制(例如,Chrome 已经这样做了),但该限制可由嵌入器配置并且不需要应用于 Node.js。

@s3ththompson感谢您的澄清。

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

相关问题

cretz picture cretz  ·  5评论

mfateev picture mfateev  ·  5评论

frehberg picture frehberg  ·  6评论

chicoxyzzy picture chicoxyzzy  ·  5评论

nikhedonia picture nikhedonia  ·  7评论