为啥 putImageData 这么慢?
Posted
技术标签:
【中文标题】为啥 putImageData 这么慢?【英文标题】:Why is putImageData so slow?为什么 putImageData 这么慢? 【发布时间】:2011-04-26 13:19:33 【问题描述】:我正在使用一个相对较大的 Canvas 来绘制各种(复杂)的东西。然后我想保存 Canvas 的状态,以便稍后将其快速重置为现在的状态。我为此使用 getImageData 并将数据存储在变量中。然后,我在画布上绘制更多内容,稍后将使用 putImageData 将画布重置为我保存状态时的位置。
但是,事实证明 putImageData 非常慢。事实上,它比简单地从头开始重绘整个 Canvas 慢,后者涉及覆盖大部分表面的多个 drawImage,以及随后的笔划和填充的 40.000 多个 lineTo 操作。
从头开始重绘大约 2000 x 5000 像素的画布大约需要 170 毫秒,但使用 putImageData 需要高达 240 毫秒。为什么 putImageData 与重绘画布相比如此缓慢,尽管重绘画布涉及用 drawImage 填充几乎整个画布,然后使用 lineTo、描边和填充再次用多边形填充大约 50% 的画布。所以基本上每个像素在重绘时至少被触摸一次。
因为 drawImage 似乎比 putImageData 快得多(毕竟,重绘画布的 drawImage 部分耗时不到 30 毫秒)。我决定尝试不使用 getImageData 来保存画布的状态,而是使用 canvas.toDataURL,然后从数据 URL 创建一个图像,我将把它粘贴到 drawImage 中以将其绘制到画布上。事实证明,整个过程要快得多,只需大约 35 毫秒即可完成。
那么为什么 putImageData 比其他方法慢得多(使用 getDataURL 或简单地重绘)?我怎样才能进一步加快速度?是否存在,如果,一般来说,存储画布状态的最佳方式是什么?
(所有数字都是使用 Firefox 中的 Firebug 测量的)
【问题讨论】:
如果你能在网上某个地方发布你的问题演示会很有趣。在 noVNC (github.com/kanaka/noVNC) 中,我将 putImageData 用于许多中小型图像数据数组,我没有看到 putImageData 存在性能问题。也许您遇到了一个特定的性能不佳的情况,应该是错误的。 你可以看这里danielbaulig.de/A3O如果firebug控制台被关闭,它不会100%工作,所以一定要打开它。签出的版本是使用 putImageData 的版本。您可以通过单击任何“图块”来触发它。它将使用 putImageData 刷新缓冲区画布,然后“突出显示”选定的图块。在 a3o_oo.js 中有一些行被注释掉,可用于在使用 putImageData(当前)、使用 getDataURL(提到 this.boardBuffer 的两行)和缓冲区画布的普通重绘(drawBoard 行)之间切换。跨度> 好问题和好解决方案。但是你有没有发现 putImageData 比 drawImage 慢的真正原因? @cherouvim 不,不是。我的假设是,主要原因是 ImageData 结构不再在硬件加速图形结构中进行管理,因此调用 getImageData/putImageData 将不得不转换到这些对象/从这些对象转换,这很慢,涉及复制大量数据,实例化对象,等等,而使用 drawImage 只是将已经存在的纹理/绘图上下文绘制到屏幕上 - 现代硬件速度非常快。 【参考方案1】:只是关于最佳方法的一个小更新。我实际上是在High Performance ECMAScript and html5 Canvas(pdf,德语;密码:***)上写了我的学士论文,所以我现在收集了一些关于这个主题的专业知识。显然最好的解决方案是使用多个画布元素。从一个画布绘制到另一个画布上的速度与将任意图像绘制到画布上一样快。因此,当使用两个画布元素时,“存储”画布的状态与稍后再次恢复它一样快。
This jsPerf testcase 非常清楚地展示了各种方法及其优缺点。
为了完整起见,这里你真的应该怎么做:
// setup
var buffer = document.createElement('canvas');
buffer.width = canvas.width;
buffer.height = canvas.height;
// save
buffer.getContext('2d').drawImage(canvas, 0, 0);
// restore
canvas.getContext('2d').drawImage(buffer, 0, 0);
根据浏览器的不同,此解决方案比获得投票的解决方案快 5000 倍。
【讨论】:
如果需要在数组中存储大量状态怎么办?是否应该创建一组画布?例如var numBuffers = 20; var tmpCan = document.createElement('canvas'); var buffers = [tmpCan]; for (var i = 1, len = numBuffers, i < numBuffers; i++) buffers.push(tmpCan.cloneNode());
或类似的东西?? 或者有更好的解决方案吗?【参考方案2】:
在 Firefox 3.6.8 中,我能够通过使用 toDataUrl/drawImage 来解决 putImageData 的缓慢问题。对我来说,它的工作速度足够快,我可以在处理 mousemove 事件时调用它:
保存:
savedImage = new Image()
savedImage.src = canvas.toDataURL("image/png")
要恢复的:
ctx = canvas.getContext('2d')
ctx.drawImage(savedImage,0,0)
【讨论】:
刚刚识别出您的回复。事实上,我目前正在以完全相同的方式进行操作 :) 此外,我目前正在尝试使用额外的隐藏画布作为缓冲区。这应该会提高创建缓冲区的性能,使用 toDataURL 相当慢,并且绘图速度应该保持大致相同(因为 drawImage 也可以将画布元素作为图像)。 刚刚意识到这个答案不断得到支持。我很欣赏这个答案,但解释的解决方案实际上在性能方面很糟糕。请自行参考接受的答案。 我正在尝试从存储在 Uint8Clamped 数组中的像素数据绘制到画布,其性能比 putImageData 更好。这似乎是实现这一目标的有希望的方法。我应该能够将 Uint8Clamped 数组转换为 DataURL,使用该 DataURL 填充一个 Image 对象,然后将该图像绘制到画布上。我还没有测试过,但我希望它会比 putImageData 更快。【参考方案3】:首先您说您正在使用 Firebug 进行测量。实际上,我发现 Firebug 会大大减慢 JS 的执行速度,因此您可能无法获得良好的性能数据。
至于putImageData
,我怀疑这是因为函数需要一个包含许多Number
对象的大型JS 数组,所有这些都必须检查范围(0..255)并复制到本机画布缓冲区中。
也许一旦 WebGL ByteArray 类型可用,这种事情可以做得更快。
base64 解码和解压缩数据(使用 PNG 数据 URL)更快,但它只使用一个 JS 字符串调用一个 JS 函数,因此它主要使用本机代码和类型。
【讨论】:
由于我的数据大部分来自本机代码执行,我怀疑 Firebug 会对它们产生重大影响。尽管如此,我们不是在谈论毫秒的分数,而是实际上但实际上是四分之一秒,基本上是单个函数调用(putImageData)。由于 JS 数组可能导致性能不佳。我将通过测试 JS 在 putImageData 之外处理(复制、操作等)这样一个数组的速度来检查这一点。 继续:解码、解压缩等不是在画布状态恢复时发生,而是在保存时发生。所以这种情况只发生一次而且我实际上没有测量它,因为保存状态需要多长时间并不是很关心。关键部分是恢复画布状态。那时我已经创建了很久的 Image 对象。因此,如果 Image 对象在本机缓冲区中包含它的数据,这确实可能是问题的原因(或者更好的是 drawImage 方法缺少它)。 我知道putImageData
的性能还可以(80-100fps 与 480x320 缓冲区) - 但您正在处理非常大的图像!
啊,+1 提到了 WebGL ByteArray。我一直在寻找有关 javascript 二进制数组的信息,您的评论帮助我找到了它。这是当前的讨论:listware.net/201009/w3c-public-webapps/…。这是建议的标准:cvs.khronos.org/svn/repos/registry/trunk/public/webgl/doc/spec/…【参考方案4】:
这大概是因为现代浏览器对<canvas>
元素使用硬件加速,而getImageData()
/putImageData()
需要将图像数据从显卡传输到主机,反之亦然。这是出了名的慢。
使用两个画布会更快,因为所有数据都保留在显卡上。
【讨论】:
以上是关于为啥 putImageData 这么慢?的主要内容,如果未能解决你的问题,请参考以下文章