Win32 位图渲染速度如何比像素快?
How is Win32 Bitmap rendering faster than pixels?
与 SetPixelV 或其他函数(例如 .如果最后计算机将为位图绘制像素,这是如何工作的?
重复调用 SetPixelV
之类的函数很慢,因为它每次都必须将坐标转换为内存偏移量,而且还可能会即时进行一些颜色转换。
一个简单的 "set pixel" 函数 可能 看起来像这样(没有边界测试、颜色转换或任何花哨的东西):
size_t offset = y * bytes_per_scanline + x * bytes_per_pixel;
for(size_t i = offset; i < offset + bytes_per_pixel; i++)
target[i] = source[i];
另一方面,位图通常是通过称为 blitting 的过程绘制的。这本质上是从一个内存位置到另一个内存位置的直接复制。要在 Windows 中实现这一点,您需要为您的位图创建一个与目标上下文 兼容 的设备上下文。这样可以确保无需翻译即可复制内存。它还可以提供更快的硬件加速副本。
一个简单的 "copy" blit 可能如下所示:
size_t nbytes = bytes_per_scanline * height;
for(size_t i = 0; i < nbytes; i++)
target[i] = source[i];
这不涉及坐标查找,并且在内存缓存访问方面非常有效。有很多更快的方法来复制内存块,上面的例子只是为了说明。
假设你有一个像素。该像素具有颜色分量 A B 和 C。您正在绘制的表面具有颜色分量 X Y 和 Z。
所以首先你需要检查它们是否匹配。如果它们不匹配,成本就会上升。假设它们匹配。
接下来,您需要进行边界检查 -- 调用者是否给了您一些愚蠢的东西?一些比较,加法和乘法。
接下来,您需要找到像素所在的位置。这是一些乘法和加法。
现在,您必须访问源数据和目标数据并将其写入。
如果您一次处理一条扫描线,几乎所有上述开销都可以一次完成。您可以计算扫描线的哪一部分落入边界或不落入边界,其开销仅比处理一个像素多一点。您可以找到扫描线写入目标的位置,而开销也仅比一个像素多一点。您可以使用与一个像素相同的开销来检查颜色 space 转换。
最大的区别在于,您不是复制一个像素,而是复制一个块。
碰巧,计算机真的很擅长复制东西块。在一些 CPU 上有内置指令,一些内存系统可以在不涉及 CPU 的情况下完成它(CPU 说 "copy X to Y",然后可以做其他事情;和内存到内存带宽可能高于内存到CPU-内存)。即使您在 CPU 中往返,也有 SIMD 指令可以让您同时处理 2、4、8、16 甚至更多的数据单元,只要您在使用有限指令集的方式相同。
在某些情况下,您甚至可以将工作卸载到 GPU——如果源扫描线和目标扫描线都在 GPU 上,您可以说 "yo GPU, you handle it",而 GPU 更适合做这种事情任务。
最开始的优化——只需对每条扫描线进行一次检查,而不是对每个像素进行一次检查——可以轻松实现 2 到 10 倍的加速。第二个——更有效的 blitting——又快了 4 到 20 倍。在 GPU 上执行所有操作的速度可以提高约 2 到 100 倍。
最后一件事是实际调用函数的开销。通常这是次要的;但是当调用 SetPixel 100 万次(一张 1000 x 1000 的图像,或中等尺寸的屏幕)时,它加起来。
对于200万像素的高清显示器来说,每秒60次就是每秒操作1.2亿像素。如果你想跟上屏幕,3 GHz 机器上的单线程程序只有 运行 每个像素约 25 条指令的空间,并且假设没有其他事情发生(这不太可能)。在 4k 显示器上,每个像素只有 6 条指令。
玩了那么多像素,去掉每一条指令,你就能产生很大的不同。
乘数不知从何而来。我已经编写了一些从每像素操作到每扫描线操作的转换,这些操作已经显示出令人印象深刻的加速,但是,CPU 到 GPU 负载的同上,并且已经看到 SIMD 提供了令人印象深刻的加速。
与 SetPixelV 或其他函数(例如 .如果最后计算机将为位图绘制像素,这是如何工作的?
重复调用 SetPixelV
之类的函数很慢,因为它每次都必须将坐标转换为内存偏移量,而且还可能会即时进行一些颜色转换。
一个简单的 "set pixel" 函数 可能 看起来像这样(没有边界测试、颜色转换或任何花哨的东西):
size_t offset = y * bytes_per_scanline + x * bytes_per_pixel;
for(size_t i = offset; i < offset + bytes_per_pixel; i++)
target[i] = source[i];
另一方面,位图通常是通过称为 blitting 的过程绘制的。这本质上是从一个内存位置到另一个内存位置的直接复制。要在 Windows 中实现这一点,您需要为您的位图创建一个与目标上下文 兼容 的设备上下文。这样可以确保无需翻译即可复制内存。它还可以提供更快的硬件加速副本。
一个简单的 "copy" blit 可能如下所示:
size_t nbytes = bytes_per_scanline * height;
for(size_t i = 0; i < nbytes; i++)
target[i] = source[i];
这不涉及坐标查找,并且在内存缓存访问方面非常有效。有很多更快的方法来复制内存块,上面的例子只是为了说明。
假设你有一个像素。该像素具有颜色分量 A B 和 C。您正在绘制的表面具有颜色分量 X Y 和 Z。
所以首先你需要检查它们是否匹配。如果它们不匹配,成本就会上升。假设它们匹配。
接下来,您需要进行边界检查 -- 调用者是否给了您一些愚蠢的东西?一些比较,加法和乘法。
接下来,您需要找到像素所在的位置。这是一些乘法和加法。
现在,您必须访问源数据和目标数据并将其写入。
如果您一次处理一条扫描线,几乎所有上述开销都可以一次完成。您可以计算扫描线的哪一部分落入边界或不落入边界,其开销仅比处理一个像素多一点。您可以找到扫描线写入目标的位置,而开销也仅比一个像素多一点。您可以使用与一个像素相同的开销来检查颜色 space 转换。
最大的区别在于,您不是复制一个像素,而是复制一个块。
碰巧,计算机真的很擅长复制东西块。在一些 CPU 上有内置指令,一些内存系统可以在不涉及 CPU 的情况下完成它(CPU 说 "copy X to Y",然后可以做其他事情;和内存到内存带宽可能高于内存到CPU-内存)。即使您在 CPU 中往返,也有 SIMD 指令可以让您同时处理 2、4、8、16 甚至更多的数据单元,只要您在使用有限指令集的方式相同。
在某些情况下,您甚至可以将工作卸载到 GPU——如果源扫描线和目标扫描线都在 GPU 上,您可以说 "yo GPU, you handle it",而 GPU 更适合做这种事情任务。
最开始的优化——只需对每条扫描线进行一次检查,而不是对每个像素进行一次检查——可以轻松实现 2 到 10 倍的加速。第二个——更有效的 blitting——又快了 4 到 20 倍。在 GPU 上执行所有操作的速度可以提高约 2 到 100 倍。
最后一件事是实际调用函数的开销。通常这是次要的;但是当调用 SetPixel 100 万次(一张 1000 x 1000 的图像,或中等尺寸的屏幕)时,它加起来。
对于200万像素的高清显示器来说,每秒60次就是每秒操作1.2亿像素。如果你想跟上屏幕,3 GHz 机器上的单线程程序只有 运行 每个像素约 25 条指令的空间,并且假设没有其他事情发生(这不太可能)。在 4k 显示器上,每个像素只有 6 条指令。
玩了那么多像素,去掉每一条指令,你就能产生很大的不同。
乘数不知从何而来。我已经编写了一些从每像素操作到每扫描线操作的转换,这些操作已经显示出令人印象深刻的加速,但是,CPU 到 GPU 负载的同上,并且已经看到 SIMD 提供了令人印象深刻的加速。