2015-06-21 61 views
4

我有一个可以反转数组中元素的循环。我已简化并将问题简化为以下内容:优化项目的反转

for (int x=0;x<w/2;++x) { 
    int il =  x; 
    int ir = w-1-x; 
    type_copy l = data[il]; 
    type_copy r = data[ir]; 
    data[il] = r; 
    data[ir] = l; 
} 

此代码反转元素,但速度较慢。首先,它不能被自动矢量化,因为数组访问是不连续的。另一方面,右侧的访问是从理想的高速缓存遍历向后的。最后,可能会有一些拖延,因为在提交最后一个循环的数据之前,下一个循环的负载不可能发生,因为编译器可能无法说明自我别名指针本身没有命中。

在我的情况,sizeof(type_copy)要么4*sizeof(uint8_t) = 4否则4*sizeof(float) = 4*4 = 16。因此请注意,字节级的反转是不可接受的。

我的问题是:这段代码如何优化,如果可以呢?

+0

这是否需要在原地? – usr2564301

+0

最终的结果是,是的。分配的临时数据是好的,但除非它是固定大小,否则我怀疑它会提高性能。 – imallett

+0

为什么不只是向后迭代而不是颠倒元素的物理顺序呢? – SU3

回答

1

假设你的数据类型,如:

struct float_data 
{ 
    float f1; 
    float f2; 
    float f3; 
    float f4; 
}; 

struct uint8_t_data 
{ 
    uint8_t f1; 
    uint8_t f2; 
    uint8_t f3; 
    uint8_t f4; 
}; 

你可以尝试上证所内部函数。对于uint8_t_data有相当不错的速度提高:

typedef uint8_t_data type_copy; 

for (int x = 0; x<w/2; x += 4) 
{ 
    int il = x; 
    int ir = w - 1 - x - 3; 

    __m128i dl = _mm_loadu_si128((const __m128i*)&data[il]); 
    __m128i dr = _mm_loadu_si128((const __m128i*)&data[ir]); 
    _mm_storeu_si128((__m128i*)&data[ir], _mm_shuffle_epi32(dl, _MM_SHUFFLE(0, 1, 2, 3))); 
    _mm_storeu_si128((__m128i*)&data[il], _mm_shuffle_epi32(dr, _MM_SHUFFLE(0, 1, 2, 3))); 
} 

输出:

g++ -O3 non vectorized: 16ms 
g++ -O3 vectorized: 5ms 

然而,对于float_data没有太大的提高速度:

typedef float_data type_copy; 

for (int x = 0; x<w/2; x+=2) { 
    int il = x; 
    int ir = w - 1 - x - 1; 

    __m256 dl = _mm256_loadu_ps((const float*)&data[il]); 
    __m256 dr = _mm256_loadu_ps((const float*)&data[ir]); 

    _mm256_storeu_ps((float*)&data[ir], _mm256_permute2f128_ps(dl, dl, 1)); 
    _mm256_storeu_ps((float*)&data[il], _mm256_permute2f128_ps(dr, dr, 1)); 

} 

输出:

g++ -O3 -mavx non vectorized: 27ms 
g++ -O3 -msse4.2 non vectorized: 25ms 
g++ -O3 -mavx vectorized: 24ms 
+0

对于不错的解决方案和性能比较+1!你有多少项目用于测试? (我在100〜4000范围内寻找更好的性能。) – imallett

+0

1024 * 1024 * 16项目。我用了足够多的东西来看看它们的差异(在相当结实的核心i7 avx2机器上测试)。 – doqtor

0

希望最好是:

for (int x=0;x<w/2;++x) { 
    std::swap(data[x], data[w-i-x]);  
} 

如果你不喜欢使用标准模板库功能,减少作业和局部变量的数目如下:

  • 删除3当地变量与您的实施相比
  • 删除了3个额外的分配操作

for (int x=0;x<w/2;++x) { type_copy l = data[x]; data[x] = data[w-1-x]; data[w-l-x] = l; }

1

你的代码不能并行很好的原因是因为有数据之间的相关性:

for (int x=0;x<w/2;++x) { 
    int il =  x; 
    int ir = w-1-x; 
    type_copy l = data[il]; 
    type_copy r = data[ir]; 
    data[il] = r; 
    data[ir] = l; 
} 

有3个操作位置:compute l/r indexesread from arraywrite to array。它们中的每一个都依赖于先前操作的结果,因此它们不能并行完成。注意我在同一类别下向左或向右分组操作。

要做的第一件事就是尝试制动依赖。

而不是在同一周期阅读广告写入尝试读取数据的迭代N和写入数据迭代N - 1;这可以在同一个循环中完成。

int il = 0; 
int ir = w-1; 
type_copy l = data[il]; 
type_copy r = data[ir]; 

for (int x=0;x<w/2;++x) { 
    data[il] = r; 
    data[ir] = l; 
    il =  x; 
    ir = w-1-x; 
    l = data[il]; 
    r = data[ir];  
} 

甚至更​​好,预先计算用于读取以及指标:

int il_0 = 0; 
int ir_0 = w-1; 
int il_1 = 1; 
int ir_1 = w-2; 
type_copy l = data[il_0]; 
type_copy r = data[ir_0]; 

for (int x=0;x<w/2;++x) { 
    data[il_0] = r; 
    data[ir_0] = l;  
    l = data[il_1]; 
    r = data[ir_1]; 
    il_0 = il_1; 
    ir_0 = ir_1;  
    il_1 = il_1++; 
    ir_1 = ir_1--; 
} 

的另一件事值得尝试的是复制一个以上的数据样本;例如在同一个周期中读取/写入2,4,...等样本。这应该会改善您的代码的并行性。

+0

+1为移位循环迭代0.5循环技巧。你也许可以添加一些关于我的原始代码中的依赖关系的解释,以及这有什么帮助?将两个指针指向左/右,并声明他们“restrict”有帮助?也许像doqtor的答案一样的性能比较? – imallett

+0

@imallett我添加了几行解释dependencyl;关于l/r限制指针 - 这不是一个坏主意。我没有指出的原因是restrict的效果高度依赖于编译器。编译器并不总是将同一缓冲区的不同位置上的两个指针视为非别名。但值得一试。 – Pandrei

0

该代码当然可以优化,但这样做可能取决于平台。例如,AMD64继承了x86 SSE的一些有用指令,包括PSHUFD或VPPERM,它们可以在矢量内任意重新排序单词,MASKMOVDQU可以组合部分写入。

+0

目前,我试图避免汇编级别的黑客攻击,赞成为自动向量化工更好。但是,如果您有任何汇编级别的技巧,他们仍然可能受到欢迎。使用SSE2定位x86和amd64。 – imallett