优化项目冲销
Optimizing Reversal of Items
我有一个循环来反转数组中的元素。我已将问题简化并减少为以下内容:
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
。因此,注意字节级反转是不可接受的。
我的问题是:如何优化这段代码,如果可以的话?
希望更好:
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;
}
你的代码不能很好并行化的原因是因为数据之间存在依赖关系:
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 indexes
、read from array
、write 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--;
}
另一件值得尝试的事情是复制多个数据样本;例如 read/write 2,4,..etc 在同一周期中的样本。这应该会提高代码的并行性。
当然可以优化代码,但这样做可能取决于平台。例如,AMD64 从 x86 SSE 继承了一堆有用的指令,包括 PSHUFD 或 VPPERM 可以在向量中任意重新排序单词和 MASKMOVDQU 可以合并部分写入。
假设您的数据类型如下:
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;
};
您可以尝试 SSE 内在函数。对于 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
我有一个循环来反转数组中的元素。我已将问题简化并减少为以下内容:
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
。因此,注意字节级反转是不可接受的。
我的问题是:如何优化这段代码,如果可以的话?
希望更好:
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;
}
你的代码不能很好并行化的原因是因为数据之间存在依赖关系:
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 indexes
、read from array
、write 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--;
}
另一件值得尝试的事情是复制多个数据样本;例如 read/write 2,4,..etc 在同一周期中的样本。这应该会提高代码的并行性。
当然可以优化代码,但这样做可能取决于平台。例如,AMD64 从 x86 SSE 继承了一堆有用的指令,包括 PSHUFD 或 VPPERM 可以在向量中任意重新排序单词和 MASKMOVDQU 可以合并部分写入。
假设您的数据类型如下:
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;
};
您可以尝试 SSE 内在函数。对于 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