【问题标题】:I'm trying to optimize this c code using 4-way loop unrolling我正在尝试使用 4 路循环展开来优化此 c 代码
【发布时间】:2009-10-01 05:42:01
【问题描述】:

我要做的是获取这个 C 代码并使用一种称为循环展开的技术对其进行优化,但在这种情况下,我想使用四向循环展开。现在,我了解了这项技术,也了解了我只是不知道如何将其应用于此代码的概念。我必须添加一些额外的变量吗?我必须在每个循环之后还是在所有循环结束时都有一些代码?此代码是 8x8 块代码,用于处理获取像素并将其逆时针旋转 90 度。任何帮助将不胜感激。谢谢。

/* 
 * rotate8 - rotate with 8x8 blocking
 */

char rotate8_descr[] = "rotate8: rotate with 8x8 blocking";

void rotate8(int dim, pixel *src, pixel *dst) 
{

int i, j, ii, jj;

for(ii = 0; ii < dim; ii += 8)
       for(jj = 0; jj < dim; jj += 8)
              for (i = ii; i < ii + 8; i++)   
                  for (j = jj; j < jj + 8; j++)
                      dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)];
}

【问题讨论】:

  • 你可能想修复语法高亮
  • 您是否对此进行了分析并确定这是一个问题?
  • 另外,考虑使用更具描述性的名称,例如“上、下、左、右”,而不是“i、ii、j、jj”。很难阅读。
  • 两个内部循环可能会通过使用 UInt64 和位移来提高速度。
  • 你为什么要在问题的末尾加上[关闭]?这并不意味着“回答”......

标签: c


【解决方案1】:

您可以用 8 行显式代码替换内部循环

          dst[RIDX(dim-1-jj, i, dim)] = src[RIDX(i, jj, dim)];
          dst[RIDX(dim-1-(jj+1), i, dim)] = src[RIDX(i, (jj+1), dim)];
          ...
          dst[RIDX(dim-1-(jj+7), i, dim)] = src[RIDX(i, (jj+7), dim)];

所以你通过显式为每个值写一行来替换循环变量。

现在您可以对下一个循环的 8 个值重复此操作,您将拥有 8 x 8 行代码,依此类推。

除了理解练习之外,这对我来说似乎毫无意义,编译器确实有效地完成了这类工作,他们会在有意义的地方进行优化。手动滚动很少产生最佳代码。

【讨论】:

  • +1 我推荐 OP profile 他的代码,看看瓶颈在哪里,然后反汇编他的代码,看看编译器是如何处理瓶颈的。
【解决方案2】:

我想说个人资料 - 但后来我自己做了。 令人惊讶的部分是 - 内循环执行得最快 布局 - 手动展开实际上更慢。

但是 - 真正的问题是 RIDX 宏。切换内存布局和切换 外循环具有显着的影响。

这是我最快的版本,带有缩进显示它与您的版本的不同之处。 假定 RIDX 宏与定义相同。

#define RIDX(x,y,d) (x+(y)*(d))
typedef unsigned char pixel;
void rotate8(int dim, pixel *src, pixel *dst)
{
    int i, j, ii, jj;
        for(jj = 0; jj < dim; jj += 8)
    for(ii = 0; ii < dim; ii += 8)
              for (i = ii; i < ii + 8; i++)
                  for (j = jj; j < jj + 8; j++)
                      dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)];
}

... 经验教训:始终配置文件 :-)

【讨论】:

    【解决方案3】:
    gcc -funrull-loops

    您不应该自己展开循环,除非 GCC 不能这样做(查看程序集),并且您已经使用探查器证明您必须加快这部分代码的速度。

    您的示例代码看起来非常适合自动展开循环。

    其他一些有用的标志:

    -O3 //开启很多优化(几乎全部) -ftree-vectorize -msse2 // 自动向量化一些循环

    【讨论】:

      【解决方案4】:

      http://www.relisoft.com/book/lang/pointer/2ptrarr.html

      如果您的编译器无法优化人类可读、可维护的算法版本,而您必须兼作人类编译器 - 购买新的编译器!再也没有人能负担得起人工编译器了。所以,请怜悯你自己和你的程序员同事,他们将不得不查看你的代码。

      【讨论】:

        【解决方案5】:

        8x8 的旋转通过 SIMD 或 SWAR 技术更有效地完成,一次可以读取至少 64 位。

        Rot90Left(X) = flip_vertical(transpose(X))
        Rot90Right(X) = transpose(flip_vertical(X))
        

        垂直翻转是一种零成本操作,因为它只意味着从临时变量的另一端存储/读取。 如果 SSE / SIMD implementation of the transpose 不能使用,这个内核在 x64 和 arm64-v8 上已经被证明相当快。

        inline void transpose_u8(uint64_t *a, uint64_t *b) {
             uint64_t A = *a, B = *b, C = B ^ (A>>8)) & 0x00ff00ff00ff00ffull;
             *a = A ^ (C << 8);
             *b = B ^ C;
        }
        inline void transpose_u16(uint64_t *a, uint64_t *b) {
             uint64_t A = *a, B = *b, C = B ^ (A>>16)) & 0x0000ffff0000ffffull;
             *a = A ^ (C << 16);
             *b = B ^ C;
        }
        inline void transpose_u32(uint64_t *a, uint64_t *b) {
             uint64_t A = *a, B = *b, C = B ^ (A>>32)) & 0x00000000ffffffffull;
             *a = A ^ (C << 32);
             *b = B ^ C;
        }
        void transpose8x8(uint8_t *src, int skip0, uint8_t *dst, int skip1) {
             uint64_t d[8];
             for (int x = 0; x < 8; x++)
                 memcpy(d+(x ^ LEFT), src + x * skip0);
             transpose_u8(d+0, d+1);
             transpose_u8(d+2, d+3);
             transpose_u8(d+4, d+5);
             transpose_u8(d+6, d+7);
             transpose_u16(d+0, d+2);
             transpose_u16(d+1, d+3);
             transpose_u16(d+4, d+6);
             transpose_u16(d+5, d+7);
             transpose_u32(d+0, d+4);
             transpose_u32(d+1, d+5);
             transpose_u32(d+2, d+6);
             transpose_u32(d+3, d+7);
             for (int x = 0; x < 8; x++)
                 memcpy(dst + x * skip1, d + (x ^ RIGHT));
        }
        
        • 这里通过设置 LEFT=0, RIGHT=7 来实现正确的旋转

        • 左旋转 == LEFT=7, RIGHT = 0

        • 转置=左=0,右=0

        我的假设是,任何体面的编译器都将通过直接修改存储在寄存器中的变量来替换 transpose_uXX 函数中的所有内部内存读取,并通过单个 64 位读取或写入内存来替换 memcpy - 这至少应该在 64 位架构中发生。在 pre-x86 上没有足够的寄存器,实际的替代方法是使用任何可用的 SIMD 寄存器和指令集。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-01-14
          • 1970-01-01
          • 2019-08-15
          • 1970-01-01
          • 2021-09-10
          相关资源
          最近更新 更多