【问题标题】:Fastest way to copy memory with stride in C?在C中跨步复制内存的最快方法?
【发布时间】:2011-09-23 07:48:37
【问题描述】:

我正在尝试尽快从 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 等。

【问题讨论】:

  • 对于 RG 值 *(uint16_t *)(dest + i) = *(short *)(source + i) 可能会有所帮助。
  • 另外,这些美元符号是怎么回事?
  • 是的,这可能确实有帮助。我会试一试,并对其进行分析 - 它可能会有所作为(我的速度是 22fps,需要 25,所以即使是很小的差异也足够了)。还有美元符号……到底是什么?!缺乏睡眠? :D 我会在任何人注意到之前进行快速编辑
  • @Chris Lutz,我认为他们可能是@psonic & 符号而不是 $ 输入错误。
  • 愚蠢的问题——我假设您已经消除了这些可能性,但是在执行 getpixels 之前是否可以使用 openGL 函数将数据展平为单色或其他东西?或者改变视频编码以期望跨步格式的数据并消除冗余副本?

标签: iphone c memory stride


【解决方案1】:

希望我参加派对还不算太晚!我刚刚使用 ARM NEON 内在函数在 iPad 上完成了类似的操作。与其他列出的答案相比,我的速度提高了 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);
}

【讨论】:

    【解决方案2】:

    您的问题仍然是真实的吗?几天前,我发布了我的 ASM 加速函数,用于大步复制字节。它比相应的 C 代码快大约两倍。您可以在这里找到它:https://github.com/noveogroup/ios-aux 可以修改为复制单词以防 RG 字节复制。

    UPD:我发现仅在调试模式下,当编译器的优化默认关闭时,我的解决方案比 C 代码更快。在发布模式下,C 代码经过优化(默认情况下),运行速度与我的 ASM 代码一样快。

    【讨论】:

    • 不,我最终设法完全优化了整个内存复制(始终是最快的解决方案!)但我会收藏它,如果我再次点击它会很有用(很有可能)。
    【解决方案3】:

    您对 ASM 满意吗?我不熟悉 ARM 处理器,但在 Analog Devices 的 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 上进行 ASM,所以“舒适” - 不。也就是说,我只需要向上看 W,所以也许我可以使用它(尽管这将是最后的手段,因为它超出了我的舒适区)。不幸的是,这里没有计算工作要做,除了与复制地址有关的任何算术。
    【解决方案4】:

    一如既往,加载和存储是最昂贵的操作。 您可以通过以下方式优化您的代码:

    • 加载一个整数 (RGBA)
    • 将所需部分存储在寄存器中(临时变量)
    • 将数据移到 temp 变量中的正确位置。
    • 执行此操作,直到本机处理器数据大小已满(在 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 家,但代价是一些额外的操作。这可以让它足够快!
    • 补充说明:确保您的数据是 int 对齐的,因为 arm 无法读取未对齐的数据。
    【解决方案5】:

    来自 Roger 的答案可能是最干净的解决方案。有一个库来保持你的代码很小总是好的。但是,如果您只想优化 C 代码,您可以尝试不同的方法。首先你应该分析你的dataSize有多大。然后,您可以进行繁重的循环展开,可能与复制 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 个字节。无论如何,我只是想向您展示如何使用只有 1 个比较的 switch 语句来结束展开的循环。 IMO 是获得体面加速的唯一方法。

    【讨论】:

    • 实际上,如果您愿意转移,您可以复制更多字节,但这可能无济于事。在我脑海中浮现:(*((short*)dst)++) = (0xFFFF0000 &amp; (*((unsigned*)src)++)) &gt;&gt; 16;
    • 如果数据长度不分为循环大小,开关功能是否有助于“剩菜”?如果是这样,这里就不需要它,但无论如何知道它是有用的。数据大小是固定的(有一些纹理大小,但我提前知道它们)。不幸的是,不是 2 的幂,但它们都是除以 1024 的“方便”数字。我将工作分成 16 个块,并同时运行它们(它用于 ipad2,所以是双核),然后分批展开 64 .
    • @psonice 是的,它只对剩菜有用。
    【解决方案6】:

    如果您对 iOS4 及更高版本没问题,您可能会发现 vDSP 和加速框架很有用。 Check out the documentation 以曲速进行各种图像处理。

    #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 仍然有用,因为有一些收集功能。在快速查看文档(就像你说的,它有点复杂!)之后,我把它留到一边,但是在那里看到你的代码,也许它并没有我想象的那么糟糕。我试试看。
    • 你只需要复制数据,还是复制后用它做点什么?
    • 只是一个直接的 RGBA -> R(或 RG)副本。处理完毕,我只需要获取正确格式的数据进行视频编码即可。
    • 嗯。在那种情况下,我不太确定 vDSP 等是否会有所帮助,当您需要一些处理时它也会得分,因为它可以执行 int 以非常快地进行两种方式的浮动转换,但在您的情况下,转换只会损害性能和其中之一其他答案将给出更好的结果。我有一种感觉,不管你怎么做,跨步的 memcpy 类型的操作基本上都会受到伤害。这是一个有趣的问题,我会再仔细考虑一下。
    • 啊。是的。我在考虑 vDSP_vgathr (developer.apple.com/library/ios/#documentation/Accelerate/…),但它会在 32 位值上运行,在这种情况下这是没用的。
    【解决方案7】:

    我更像是一个 while 家伙 -- 你可以将其转换为 for,我确定

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

    至于更快,你必须测量。

    【讨论】:

    • 谢谢,将尝试并配置文件(可能结合其他一些建议)。
    【解决方案8】:

    根据编译后的代码,您可能希望通过添加第二个循环索引将乘法替换为 2(称为 j 并将其提前 4):

    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];
    }
    

    【讨论】:

    • 我还没有检查编译器为此发出了什么(而且我对 ARM 汇编器的了解不够),但总的来说,乘法非常昂贵。位移是一个有效的优化。我会试一试(虽然上面的代码并不完美)。
    • 恕我直言,用 shift 代替乘法是不好的建议。这是编译器关心的问题。
    • 很有帮助。通常编译器关心的问题绝对是我刚才关心的问题,所以我会同时尝试和 profile。即使是很小的差异也可能就足够了。
    • 实际上,对于当今任何 1/4 的处理器来说,乘法实际上是一条原生指令,通常可以在与加法相同的周期内完成。
    • @duedl0r - 我的朋友,你在这里错了。尽管非常先进,但编译器的启发式方法有限。通常,为了利用架构的优势和优势,需要以特定的方式编写他的代码,以便提示编译器如何生成最佳代码。
    猜你喜欢
    • 2018-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-10
    • 2012-07-02
    相关资源
    最近更新 更多