关于提高函数性能的建议

Advice on improving a function's performace

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

我正在寻找有关执行此操作的最佳方法的提示,因为我使用的功能可以在不到 1.5 毫秒的时间内将 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. 使用 library/os 函数为我做这个

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

哎呀,有点啰嗦。很抱歉,但我会非常感谢任何指点。

额外说明:我正在通过 DRI/M 接口编写 for/on Linux 嵌入式设备。


编辑

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

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

编译器:GCC/G++ 11.2.0

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

您的代码主要 受内存操作限制 ,更具体地说 memcpy 因为编译器(如 GCC 11)已经积极优化它。 memcpy 通常非常有效地实施。话虽这么说,但有时 sub-optimal。我认为这里就是这种情况。要理解原因,我们需要深入研究主流现代处理器的 low-level 架构,更具体地说,是 缓存层次结构 .

有两个主要writing cache policy在主流处理器中广泛使用:write-back策略(write-allocate)和write-through 政策(没有 write-allocate)。 Raspberry Pi 3B 的英特尔处理器和 BCM2837 (Cortex-A53) 使用 write-back 策略和 write-allocate。写入分配导致 missed-write 位置的数据被加载到缓存,然后是 write-hit 操作。问题是 写入 last-level 缓存 (LLC) 的缓存行需要先从 RAM 中读取,然后再写回 。这可能导致多达一半的带宽被浪费!

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

memcpy 一般设计为在复制大缓冲区时使用 non-temporal 指令。但是,当复制许多小缓冲区时,memcpy 无法知道整个缓冲区集是否太大而无法放入 LLC 甚至写入的数据不适合很快被重用。事实上,有时甚至开发人员也不知道,因为计算缓冲区的大小可能取决于用户,而 LLC 的大小取决于用户的目标机器。

坏消息是这样的指令很难在 high-level 代码中使用。 ICC 支持使用 pragma 指令,Clang 对内置函数有实验性支持,而 AFAIK GCC 尚不支持。 OpenMP 5 为此提供了可移植的 pragma 指令,但尚未实现。更多信息,请阅读this post.

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

在 ARMv8 处理器上,有一条指令 STNP 但这只是一个提示,并不是真正要绕过缓存。此外, Cortex-A53 specification seems to only support non-temporal loads (not non-temporal stores). Put it shortly, .

好消息是最近的 ARMv9 指令集为此添加了新指令。事实上,它为 non-temporal 副本添加了特定说明。您可以获得规格 here.

由于 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,这接近您报告的执行时间。同样重要的是要提到 read-write DRAM 访问往往比普通读取或普通写入慢一点。如果 STNP 提示运行良好,它将产生 0.8 毫秒(在此硬件上最佳)。 如果您想要更快的执行时间,那么您需要处理较小的图像

在你的 Dell 机器上有 Intel Kaby-Lake 处理器和 1~2 个 2400MHz DDR4 内存通道,实际吞吐量应该是 12~35 GiB/s 导致 0.1~0.3 ms 复制带有基本 memcpy 的图像和带有 non-temporal 指令的 0.1~0.2 毫秒。此外,LLC 缓存更大,因此只要图像适合 LLC(肯定小于 0.1 毫秒),基本 memcpy 的操作应该更快。