【问题标题】:Copy memory every n bytes每 n 个字节复制一次内存
【发布时间】:2021-04-27 06:11:10
【问题描述】:

我有一个 uint8_t 值数组,我的目标是每 3 个字节复制一个 dst 数组,但问题是我在 dst 数组中从 4 个字节迭代到 4 个字节,如下所示。

src = {1,2,3,4,5,6};
dst = {0,0,0,0,0,0,0,0};
...
dst = {1,2,3,0,4,5,6,0}

现在我正在使用以下代码来执行此任务。

for(int i=0; i<arr_size ; i++)
    memcpy(dst + i*4, arr_ptr + i*3, 3);

有没有更快/更有效的方法来做到这一点?

编辑以获取更多上下文:
我有以下结构,需要用图像数组中的数据填充,其中a 将始终用 0 初始化。

typedef struct {unsigned char r,g,b,a} uchar4;
...
// init dst
...
*dst = (uchar4 *)malloc(height * width * sizeof(uchar4));

通过为 uchar4 数组赋值struct.variable = value,需要很多时间,这让我认为将存储 uint8_t 值的图像数组中的值复制到 uchar4 数组会更快, 因为 uchar 和 uint8 在内存中占用 1 个字节。这样,structs 数组用 0 初始化,扁平图像中的每 3 个字节在 uchar arr 中每 4 个字节粘贴一次。

Edit2:代码更正

【问题讨论】:

  • 这似乎更适合the Code Review SE
  • @Someprogrammerdude 在建议用户在 CR 上发帖时,如果还有类似“请阅读相关帮助中心页面,例如 'What topics can I ask about here?' 和 'How do I ask a good question?”。在当前的形式中,上面的代码可能会因为题外话而被关闭,因为它经常发生is missing context
  • memcpy(dst + i*3, arr_ptr + i*4, 3); 它不会像你想象的那样做
  • 是的,我认为您在 memcpy 中将 3 和 4 颠倒了。而且我不认为arr_size 是循环的正确上限。但我强烈怀疑,即使是没有memcpy 的幼稚实现,就像 Erdal 的回答一样,也会被一个体面的编译器很好地优化。
  • @SᴀᴍOnᴇᴌᴀ 从好的方面来说,一些程序员老兄确实链接到 CR 的“我可以在这里询问哪些主题?”而不是主要的 CR 页面。我通常会做同样的事情(让提问者更有可能看到该页面)。

标签: c++ c memory memory-management


【解决方案1】:

有很多方法可以尝试优化您的转化循环。正如 0___________ 所建议的那样,您应该考虑 memcpy 用于大小大小的块,因为大多数优化器将为目标平台生成非常有效的代码,击败手工编码的幼稚替代方案。

这是一个比较 3 种方法的快速基准测试:

  • 显式复制 3 个字节,递增指针。
  • 为此副本使用memcpy

可以添加其他方法,例如尝试利用 SIMD 指令,这应该以牺牲可移植性为代价提供显着的性能改进。

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

typedef struct rgb {
    uint8_t r, g, b;
} rgb;

typedef struct rgba {
    uint8_t r, g, b, a;
} rgba;

void copy3to4_simple(void *to, const void *from, size_t count) {
    const uint8_t *src = from;
    uint8_t *dst = to;
    uint8_t *end = dst + count * 4;
    while (dst < end) {
        dst[0] = src[0];
        dst[1] = src[1];
        dst[3] = src[2];
        dst += 4;
        src += 3;
    }
}

void copy3to4_memcpy(void *to, const void *from, size_t count) {
    const uint8_t *src = from;
    uint8_t *dst = to;
    for (size_t i = 0; i < count; i++) {
        memcpy(dst + i * 4, src + i * 3, 3);
    }
}

int main() {
    int width = 1920, height = 1080;
    rgb *src = calloc(sizeof(*src), width * height);
    rgba *dst = calloc(sizeof(*dst), width * height);
    const char *name[10];
    clock_t c[10];
    int n = 0;

#define RUNS  100
    name[n] = "simple";
    for (int i = 0; i < RUNS + 10; i++) {
        if (i == 10)
            c[n] = -clock();
        copy3to4_simple(dst, src, width * height);
    }
    c[n++] += clock();

    name[n] = "memcpy";
    for (int i = 0; i < RUNS + 10; i++) {
        if (i == 10)
            c[n] = -clock();
        copy3to4_memcpy(dst, src, width * height);
    }
    c[n++] += clock();

    for (int i = 0; i < n; i++) {
        printf("%s: %.3f msec\n", name[i], c[i] * 1000. / CLOCKS_PER_SEC / RUNS);
    }
    free(src);
    free(dst);
    return 0;
}

在我的旧 Macbook 上运行它,我得到了这个:

simple: 2.478 msec
memcpy: 1.840 msec

memcpysimple 高 25%,但在不同的架构上您可能会得到不同的结果。

【讨论】:

  • 我刚刚在我的机器上尝试了 copy3to4_simple,它比 memcpy 快了约 1 毫秒,因为它们花费的时间非常相似,我会关注两者,因为它们将在另一台机器上实现,这顺便说一句,运行英特尔。如果您碰巧知道任何其他改进方法,我会很高兴(您提到了 SIMD 指令)。
  • @GustavoStahl:我刚刚在copy3to4_simple() 上发布了一个小改进(在我的机器上):将dst 与数组末尾进行比较,而不是递减count
【解决方案2】:

我假设arr_size 是要复制的三元组数。

for(size_t i=0; i<arr_size ; i++)
    memcpy(dst + i*3, src + i*4, 3);

一定是错的

for(size_t i=0; i<arr_size ; i++)
    memcpy(dst + i*4, src + i*3, 3);

现在是上下文。

typedef struct {unsigned char r,g,b,a} uchar4;

不保证编译器不会添加任何填充。并且任何指针双关语都可能无法正常工作。添加静态断言以检查结构的大小为 4,如果不使用,则需要使用一些编译器扩展来打包结构。

效率: 很难判断,但此处答案中带有代码的琐碎函数表明,memcpy 版本很可能是最有效的。

https://godbolt.org/z/E4s8sa

我试图删除一个内存访问并且写得非常糟糕(它通常会调用 UB!但它可以在 X86 和 Cortex-M3 及更高版本上工作)。只是为了好奇:(警告!!图形编程内容!!!不适合所有观众)https://godbolt.org/z/Pefc6T

【讨论】:

  • @chqrlie 我假设 array_size 是要复制的三元组的数量。如果我们考虑上下文,IMO 是合乎逻辑的。
  • 有道理,像素数组的大小。
【解决方案3】:

你根本不需要memcpy。只需使用指针算法,您就可以执行以下操作:

uint8_t *src = some_values;
uint8_t *end = src + some_values_size;
uint8_t *dst = some_buffer;

for (; src < end; src += 3, dst += 4) {
    dst[0] = src[0];
    dst[1] = src[1];
    dst[2] = src[2];
}

通过上面的示例,您可以将代码定义为宏并将其用于不同的数据类型。 memcpy 想知道它需要复制多少字节,因此你需要一个类型。

注意:代码假设数组src的长度是3的倍数,数组dst的长度等于:(length(src) / 3) * 4

【讨论】:

  • 如果 src 是 31 long 那么 src[2] 将读取过去的数组 (src[32])。
  • @Notinlist 是的,你说得对。但我假设源数组是一个包含 3 个分量的向量数组,因此它的长度是 3 的倍数。
  • 逐字节复制比memcpygodbolt.org/z/9GhcK5慢。不要害怕memcpy,要害怕微优化,因为它们经常以与预期相反的方式工作。在memcpy 我们信任
  • 我没有说使用 memcpy 会更慢。我刚才说了,没必要。而不使用它的好处是可以将代码用作宏(针对不同的数据类型),而无需指定要复制的字节数。
  • @all 我做了一些测试。对于 unsigned char,memcpy 获胜,但对于其他类型(int、long),memcpy 失败。
猜你喜欢
  • 2018-01-06
  • 2023-04-09
  • 2014-02-12
  • 2023-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-25
  • 1970-01-01
相关资源
最近更新 更多