在C语言中,带有跨度的最快内存复制方式是什么?

12
我正在尝试尽可能快地复制RGBA图像数据中的1或2个颜色通道(这是我的代码最慢的部分,它减缓了整个应用程序的速度)。是否有一种快速的方式来进行跨距复制?
数据简单地排列为RGBARGBARGBA等,我需要复制仅R值,或者在另一种情况下,仅RG值。
目前我所拥有的大致上是为了复制R值:
for(int i=0; i<dataSize; i++){
    dest[i] = source[i*4];
}

对于RG值,我正在执行以下操作:

for(int i=0; i<dataSize; i+=2){
    dest[i] = source[i*2];
    dest[i+1] = source[(i*2)+1];
}

所有数据都是无符号的1字节值。是否有更快的方法?我已经部分展开了循环(每次迭代处理64个值 - 超过这个数量会得到微不足道的加速)。平台为Armv7(iOS),因此使用NEON(SIMD)可能很有用,但很遗憾我没有任何经验!

不幸的是,改变数据是不可能的,因为它是由OpenGL的readPixels()函数提供的,而且据我所知,iOS不支持读取L、LA、RG等格式。


3
对于 RG 值,*(uint16_t *)(dest + i) = *(short *)(source + i) 可能有帮助。 - Chris Lutz
1
还有,那些美元符号是怎么回事? - Chris Lutz
@Chris Lutz,我认为@psonic可能打错了字,应该是$符号而不是&符号。 - Tirth
1
愚蠢的问题——我假设您已经排除了这些可能性,但在执行getpixels之前是否有可能使用OpenGL函数将数据压平为单色或其他形式?或者更改视频编码以期望以步幅格式提供数据并消除冗余复制? - Jack V.
1
顺便提一下,现代处理器具有零开销循环机制(ZOL),这意味着除了第一个设置周期外,测试和分支是在硬件中完成的,因此没有惩罚。这就是为什么您在展开循环时几乎看不到任何改进的原因。展开是一种有用的实践,但用于其他目的(即不是为了保存循环迭代)。 - ysap
显示剩余3条评论
8个回答

5

如果您使用的是iOS4及以上版本,您可能会发现vDSP和加速框架非常有用。请查看文档,了解各种图像处理技巧的高速实现。

#import <Accelerate/Accelerate.h>

我不知道你接下来要做什么,但如果你要对图像数据进行任何形式的计算,并且希望以浮点形式呈现,你可以使用vDSP_vfltu8将源字节数据的一个通道转换为单精度浮点数,只需一行代码(不包括内存管理)。

vDSP_vfltu8(srcData+0,4,destinationAsFloatRed,1,numberOfPixels)
vDSP_vfltu8(srcData+1,4,destinationAsFloatGreen,1,numberOfPixels)
vDSP_vfltu8(srcData+2,4,destinationAsFloatBlue,1,numberOfPixels)
vDSP_vfltu8(srcData+3,4,destinationAsFloatAlpha,1,numberOfPixels)

如果您需要从处理过的浮点数据创建图像,则可以使用vDSP_vfuxu8来反向操作,如下:
vDSP_vfixu8(destinationAsFloatRed,1,outputData+0,4,numberOfPixels);
vDSP_vfixu8(destinationAsFloatGreen,1,outputData+1,4,numberOfPixels);
vDSP_vfixu8(destinationAsFloatBlue,1,outputData+2,4,numberOfPixels);
vDSP_vfixu8(destinationAsFloatAlpha,1,outputData+3,4,numberOfPixels);

显然,您可以使用上述技术处理1或2个通道。虽然文档很复杂,但结果很好。

我正在使用GLSL在GPU上进行所有的重负载操作,并已经将该方面优化到了极致。'慢'的部分仅仅是从纹理中获取数据并且删除不需要的通道,因为readPixels()仅支持RGBA。然而,我认为vDSP仍然可能有用,因为有一些gather函数。我曾经把它放在一边,只是快速查看了文档(就像你说的,它有点复杂!)但是看到你的代码后,也许它并没有我想象的那么糟糕。我会试一试。 - user816936
你只需要复制数据,还是在复制后要对其进行操作? - Roger
只需要直接将RGBA -> R(或RG)复制即可。处理已经完成,我只需要将数据以正确的格式传递给视频编码器。 - user816936
嗯,那样的话我就不太确定 vDSP 等工具是否有帮助了。因为它可以快速地进行整型到浮点型的转换,所以在需要一些处理时它会得分,但在你的情况下,这种转换只会降低性能,而其他答案将会给出更好的结果。我有一种感觉,无论如何进行 strided memcpy 类型的操作都会受到影响。这是一个有趣的问题,我会再考虑一下。 - Roger
啊,是的。我在考虑 vDSP_vgathr (http://developer.apple.com/library/ios/#documentation/Accelerate/Reference/vDSPRef/Reference/reference.html#//apple_ref/doc/uid/TP40009464),但它将对32位值进行操作,在这种情况下没用。 - user816936

3

一如既往,加载和存储是最耗费时间的操作。您可以按照以下方式优化您的代码:

  • 加载一个int(RGBA)
  • 将所需部分存储到寄存器中(临时变量)
  • 将数据移动到临时变量中的正确位置。
  • 重复以上步骤,直到本机处理器数据大小满足条件(在32位机器上为4倍字符长度)
  • 将临时变量存储到内存中。

这段代码只是快速输入,以便传达思想。

unsigned int tmp;
unsigned int *dest;

for(int i=0; i<dataSize; i+=4){
    tmp  = (source[i] & 0xFF);
    tmp |= (source[i+1] & 0xFF) << 8;
    tmp |= (source[i+2] & 0xFF) << 16;
    tmp |= (source[i+3] & 0xFF) << 24;

    *dest++ = tmp;
}

我认为你是对的,特别是这种处理方式不太适合缓存。最好的情况是,我可以从4个存储器减少到1个,也许从2次加载减少到1次,代价是多做一些操作。这可能足以使它变得足够快! - user816936
作为附加注释:请确保您的数据对齐为int类型,因为arm无法读取未对齐的数据。 - wpaulus

2

根据编译代码,您可能希望用第二个循环索引(称其为j并将其增加4)的添加来替换乘以2的操作:

for(int i=0, j=0; i<dataSize; i+=2, j+=4){
    dest[$i] = source[$j];
    dest[$i+1] = source[$j+1];
}

或者,您可以将乘法替换为向左移位 1 位:

for(int i=0, j=0; i<dataSize; i+=2, j+=4){
    dest[$i] = source[$i<<1];
    dest[$i+1] = source[($i<<1)+1];
}

3
个人认为用移位代替乘法是不好的建议。这是编译器的责任。 - duedl0r
有帮助。通常编译器关注的问题现在完全是我的问题,所以我会尝试两种方法并进行分析。即使是微小的差异也可能足够了。 - user816936
@ysap 我也不同意你的编译器优化评论。编译器现在已经不再愚蠢了.. :) - duedl0r
@duedl0r -LOL,“也许是你的硬件问题”...在设计时,TigerSHARC是DSP中的劳斯莱斯...我不知道Pentium,我是一名DSP工程师。英特尔可能做出了自己的选择(顺便说一句,你刚刚提到了一个在英特尔架构上用移位替换乘法的理由)。与其争论,不如浏览Texas Instruments(www.ti.com)、Analog Devices(www.analog.com)、ARM(www.arm.com)和其他任何嵌入式处理公司,并自行查看,如果您知道如何阅读硬件参考手册的话。 - ysap
@duedl0r - 并且澄清一下 - 你是对的,mul(乘法)比addition(加法)需要更多的硬件/时间。奔腾处理器之所以在几年前就达到4GHz的速度,而领先的DSP(数字信号处理器)刚刚进入1GHz领域,原因在于奔腾处理器具有极深的流水线 - 这使其能够达到如此高的速度 - 可能他们决定为不同的指令实现不同的延迟。 - ysap
显示剩余9条评论

2

我更喜欢使用while循环 -- 你可以将其转换为for循环,我相信你一定行。

i = j = 0;
while (dataSize--) {
    dst[i++] = src[j++]; /* R */
    dst[i++] = src[j++]; /* G */
    j += 2;              /* ignore B and A */
}

关于它是否更快,需要进行测量。


谢谢,我会尝试并进行性能分析(可能与其他建议相结合)。 - user816936

1
Roger的答案可能是最干净的解决方案。拥有一个库来保持代码简洁总是很好的。但如果你只想优化C代码,你可以尝试不同的方法。首先,你应该分析一下你的数据大小。然后,你可以进行大量的循环展开,可能结合复制int而不是字节:(伪代码)
while(dataSize-i > n) { // n being 10 or whatever
   *(int*)(src+i) = *(int*)(dest+i); i++; // or i+=4; depending what you copy
   *(int*)(src+i) = *(int*)(dest+i);
   ... n times
}

然后用以下代码完成剩余部分:

switch(dataSize-i) {
    case n-1: *(src+i) = *(dest+i); i++;
    case n-2: ...
    case 1: ...
}

代码看起来有点丑陋,但是它确实很快 :)

如果你知道dataSize的行为,甚至可以进行更多优化。也许它总是2的幂?或者是偶数?


我刚刚意识到你不能一次复制4个字节,只能复制2个字节。无论如何,我只是想向你展示如何使用一个比较语句结束未展开的循环。在我看来,这是获得相当不错加速的唯一方法。


实际上,如果你愿意移位,你可以复制更多的字节,但这可能并没有什么帮助。我能想到的是:(*((short*)dst)++) = (0xFFFF0000 & (*((unsigned*)src)++)) >> 16; - Jon Purdy
开关函数是否有助于处理数据长度不能被循环大小整除的“剩余部分”?如果是这样,那么在这里它是不需要的,但了解这一点仍然很有用。此数据大小为固定值(有几个纹理大小,但我提前都知道)。不幸的是,它们都不是2的幂,但它们都是可以被1024整除的“方便”数字。我将工作分成16个块,并同时运行它们(这是为iPad2设计的,因此具有双核),然后批量展开64个。 - user816936
@psonice 是的,它只对剩余物有用。 - duedl0r

1
希望我没有来晚!我在iPad上使用ARM NEON intrinsics完成了类似的事情。与其他列出的答案相比,我获得了2-3倍的速度提升。请注意,下面的代码仅保留第一个通道,并要求数据为32字节的倍数。
uint32x4_t mask = vdupq_n_u32(0xFF);

for (unsigned int i=0, j=0; i < dataSize; i+=32, j+=8) {

    // Load eight 4-byte integers from the source
    uint32x4_t vec0 = vld1q_u32((const unsigned int *) &source[i]);
    uint32x4_t vec1 = vld1q_u32((const unsigned int *) &source[i+16]);

    // Zero everything but the first byte in each of the eight integers
    vec0 = vandq_u32(vec0, mask);
    vec1 = vandq_u32(vec1, mask);

    // Throw away two bytes for each of the original integers
    uint16x4_t vec0_s = vmovn_u32(vec0);
    uint16x4_t vec1_s = vmovn_u32(vec1);

    // Combine the remaining bytes into a single vector
    uint16x8_t vec01_s = vcombine_u16(vec0_s, vec1_s);

    // Throw away the last byte for each of the original integers
    uint8x8_t vec_o = vmovn_u16(vec01_s);

    // Store to destination
    vst1_u8(&dest[j], vec_o);
}

1

您的问题仍然存在吗?我几天前发布了一个使用ASM加速的字节复制函数,比相应的C代码快大约两倍。您可以在这里找到它:https://github.com/noveogroup/ios-aux 如果需要复制RG字节,则可以修改它以复制单词。

更新:我发现我的解决方案只有在默认情况下关闭编译器优化的调试模式下才比C代码快。在发布模式下,C代码被优化(默认情况下),并且与我的ASM代码一样快。


不,最终我成功地完全优化了整个内存复制(始终是最快的解决方案!)但我会将其加为书签,如果我再次遇到这种情况(很有可能),它会很有用。 - user816936

0
你熟悉汇编语言吗?我不熟悉ARM处理器,但在模拟设备的Blackfin上,这个复制实际上是免费的,因为它可以并行进行计算操作。
i0 = _src_addr;
i1 = _dest_addr;
p0 = dataSize - 1;

r0 = [i0++];
loop _mycopy lc0 = p0;
loop_begin _mycopy;
    /* possibly do compute work here | */ r0 = [i0++] | W [i1++] = r0.l;
loop_end _mycopy;
W [i1++] = r0.l;

所以,你每个像素有1个周期。请注意,这对于RG或BA复制来说是很好的。正如我所说,我不熟悉ARM,也完全不了解iOS,所以我不确定你是否可以访问ASM代码,但你可以尝试寻找那种优化。


好的,我上一次使用汇编语言是在1993年的6502上,所以“熟练”-不是很熟练。话虽如此,我只需要查找W,所以也许我可以使用它(但这将是最后的选择,因为它超出了我的舒适区)。不幸的是,在这里没有任何计算工作要做,除了与复制地址有关的任何算术运算。 - user816936

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接