关于提高函数性能的建议



对于我正在处理的项目,我需要一个函数,该函数可以通过像素缓冲区将矩形图像的内容复制到另一个图像中。该函数需要考虑目标图像上的边缘冲突,因为这两个图像的大小很少相同。

我正在寻找最优化的方法,因为我使用的功能可以在不到1.5ms的时间内将720x480图像复制到1920x955图像中。这本身很好,但很难优化。

#define coord(x, y) ((void *) (dest + 4 * ((y) * width + (x))))
#define scoord(x, y) ((void *) (src + 4 * ((y) * src_width + (x))))
void copy_buffer(uint8_t* dest, int width, int height, uint8_t* src, int src_width, int src_height, int x, int y) {
if (x + src_width < 0 || x >= width || y + src_height < 0 || y >= height || src_width <= 0 || src_height <= 0)
return;
for (int line = std::max(0, y); line < std::min(height, y + src_height); line++)
memcpy(coord(std::max(0, x), line), scoord(-1 * std::min(0, x), -1 * std::min(0, y)), (std::min(x + src_width, width) - std::max(0, x)) * 4);
}

我考虑过的一些事情

  1. 多线程似乎不太理想,原因有几个;

    • 竞争同时访问同一存储器区域的条件
    • 生成和管理独立线程的开销
  2. 使用我的系统的GPU

    • 有效的多线程
    • 在GPU和CPU之间移动和管理数据的巨大开销
    • 无法移植到我的目标平台
  3. 算法优化,如计算多图像边界框和添加***,会加载更多的代码,只渲染图像中可见的区域

    • 虽然我无论如何都在计划这样做,但我想我应该在这里提到它,以了解如何最好地实现这一目标的进一步信息
  4. 使用库/os函数为我做这件事

    • 我对底层编程很陌生,尤其是面向性能的编程,所以我总是有可能错过一些东西
    • 我目前没有使用像SFML这样的多媒体框架,因为我试图专注于可执行文件和代码库大小,但如果这是最好的想法,那就顺其自然吧

呼,有点咬。我很抱歉,但如果有任何建议,我将不胜感激。

额外说明:我正在为DRI/m接口上的Linux嵌入式设备编写。


编辑

根据@Jérôme Richard的评论,关于我的系统的一些信息

开发机器:戴尔inspiron 15 7570,16GB RAM,i7 8core+Ubuntu 21.04目标机器:Raspberry Pi 3B(1GB RAM,Broadcom或其他)4核1.4GHz+用于Pi 的Ubuntu服务器

编译器:GCC/G++11.2.0

您的代码主要受内存操作的限制,更具体地说是memcpy,因为编译器(如GCC 11)已经对其进行了积极的优化。CCD_ 2通常被非常有效地实现。也就是说,它有时是次优的。我认为这里就是这样。为了理解原因,我们需要深入研究主流现代处理器的底层架构,更具体地说是缓存层次结构

主流处理器中广泛使用两种主要的写缓存策略:回写策略(带写分配)和直写回策略和写分配。写入分配导致将未写入位置的数据加载到缓存,然后执行写入命中操作。问题是写入最后一级缓存(LLC)的缓存行需要先从RAM中读取,然后再写回。这可能会导致多达一半的带宽被浪费!

当大数组被写入RAM(不适合LLC)时,为了解决这个问题,设计了非临时指令。这种指令的目标是直接写入RAM并绕过缓存层次结构,以免造成缓存污染并更好地使用内存带宽。

CCD_ 3通常被设计为在复制大缓冲区时使用非临时指令。然而,当复制许多小缓冲区时,memcpy无法知道整个缓冲区太大而无法放入LLC,甚至无法知道写入的数据不会很快被重用。事实上,有时,即使是开发人员也无法知道,因为计算的缓冲区的大小可能取决于用户,而LLC的大小取决于用户目标机器。

坏消息是,这样的指令很难从高级代码中使用。ICC支持使用pragma指令,Clang有一个内置的实验性支持,而AFAIK-GCC还不支持。OpenMP 5为此提供了一个可移植的pragma指令,但尚未实现。欲了解更多信息,请阅读这篇文章。

在x86-64处理器上,可以使用SSE和AVX内部函数_mm_stream_si128_mm256_stream_si256。请注意,指针需要在内存中对齐。

在ARMv8处理器上,有一条指令STNP,但这只是一个提示,实际上并不是要绕过缓存。此外,Cortex-A53规范似乎只支持非临时加载(而非临时存储)。简而言之,ARMv8没有这样的指令。

好消息是,最近的ARMv9指令集为此添加了新的指令。事实上,它为非临时拷贝添加了特定的指令。你可以在这里得到规格。

由于Raspberry Pi 3B实用内存基准测试表明读取带宽为2.7 GB/s,写入带宽为2.4 GB/s。具有写入分配的720x480 3通道映像的副本大约需要720 * 480 * 3 * (1/2.4e9 + 2/2.7e9) = 1.2 ms,这与您报告的执行时间接近。同样重要的是,读写DRAM访问往往比普通读取或普通写入慢一点。如果STNP提示工作良好,它将导致0.8毫秒(在该硬件上是最佳的)如果您想要更快的执行时间,那么您需要处理较小的图像

在使用Intel Kaby Lake处理器的Dell机器上,当然还有1~2个2400MHz DDR4内存通道,实际吞吐量应为12~35 GiB/s,使用基本memcpy复制图像的时间应为0.1~0.3 ms,使用非临时指令的时间应在0.1~0.2 ms。此外,LLC缓存要大得多,因此只要图像适合LLC(当然小于0.1ms),基本memcpy的操作就应该更快。

您可以一次性确定源图像的哪个矩形将有效地复制到目标。然后,最有效的方法是逐行复制,因为行是连续的。memcpy是最快的方式。

最新更新