最初问题出现在我尝试optimize an algorithm用于霓虹灯臂时,其中一些小部分是根据剖析器占80%。我试着测试一下可以做些什么来改进它,为此我创建了一系列函数指针到我的优化函数的不同版本,然后我在循环中运行它们以在profiler中查看哪一个表现更好:
typedef unsigned(*CalcMaxFunc)(const uint16_t a[8][4], const uint16_t b[4][4]);
CalcMaxFunc CalcMaxFuncs[] =
{
CalcMaxFunc_NEON_0,
CalcMaxFunc_NEON_1,
CalcMaxFunc_NEON_2,
CalcMaxFunc_NEON_3,
CalcMaxFunc_C_0
};
int N = sizeof(CalcMaxFunc) / sizeof(CalcMaxFunc[0]);
for (int i = 0; i < 10 * N; ++i)
{
auto f = CalcMaxFunc[i % N];
unsigned retI = f(a, b);
// just random code to ensure that cpu waits for the results
// and compiler doesn't optimize it away
if (retI > 1000000)
break;
ret |= retI;
}
我得到了令人惊讶的结果:函数的性能完全取决于它在CalcMaxFuncs数组中的位置。例如,当我将CalcMaxFunc_NEON_3交换为第一时它会慢3-4倍,根据分析器,它会在函数的最后一位停止,它试图将数据从氖移动到arm寄存器。
那么,它有时会导致什么停滞而不是在其他时间?顺便说一句,如果重要的话,我会在iPhone上用xcode进行配置。
当我故意通过在循环中调用这些函数之间混合一些浮点分区来引入氖管道停顿时,我消除了不可靠的行为,现在它们都执行相同的操作,而不管它们被调用的顺序如何。那么,为什么首先我遇到了这个问题,我该怎么做才能在实际代码中消除它?
更新 我尝试创建一个简单的测试功能,然后分阶段进行优化,看看我怎么可能避免霓虹灯和手臂档位。 这是测试运行功能:
void NeonStallTest()
{
int findMinErr(uint8_t* var1, uint8_t* var2, int size);
srand(0);
uint8_t var1[1280];
uint8_t var2[1280];
for (int i = 0; i < sizeof(var1); ++i)
{
var1[i] = rand();
var2[i] = rand();
}
#if 0 // early exit?
for (int i = 0; i < 16; ++i)
var1[i] = var2[i];
#endif
int ret = 0;
for (int i=0; i<10000000; ++i)
ret += findMinErr(var1, var2, sizeof(var1));
exit(ret);
}
findMinErr
就是这样:
int findMinErr(uint8_t* var1, uint8_t* var2, int size)
{
int ret = 0;
int ret_err = INT_MAX;
for (int i = 0; i < size / 16; ++i, var1 += 16, var2 += 16)
{
int err = 0;
for (int j = 0; j < 16; ++j)
{
int x = var1[j] - var2[j];
err += x * x;
}
if (ret_err > err)
{
ret_err = err;
ret = i;
}
}
return ret;
}
基本上它是每个uint8_t [16]块之间的平方差的和,并返回具有最小平方差的块对的索引。所以,然后I rewrote it in neon intrisics(没有特别尝试使它快速,因为它不是重点):
int findMinErr_NEON(uint8_t* var1, uint8_t* var2, int size)
{
int ret = 0;
int ret_err = INT_MAX;
for (int i = 0; i < size / 16; ++i, var1 += 16, var2 += 16)
{
int err;
uint8x8_t var1_0 = vld1_u8(var1 + 0);
uint8x8_t var1_1 = vld1_u8(var1 + 8);
uint8x8_t var2_0 = vld1_u8(var2 + 0);
uint8x8_t var2_1 = vld1_u8(var2 + 8);
int16x8_t s0 = vreinterpretq_s16_u16(vsubl_u8(var1_0, var2_0));
int16x8_t s1 = vreinterpretq_s16_u16(vsubl_u8(var1_1, var2_1));
uint16x8_t u0 = vreinterpretq_u16_s16(vmulq_s16(s0, s0));
uint16x8_t u1 = vreinterpretq_u16_s16(vmulq_s16(s1, s1));
#ifdef __aarch64__1
err = vaddlvq_u16(u0) + vaddlvq_u16(u1);
#else
uint32x4_t err0 = vpaddlq_u16(u0);
uint32x4_t err1 = vpaddlq_u16(u1);
err0 = vaddq_u32(err0, err1);
uint32x2_t err00 = vpadd_u32(vget_low_u32(err0), vget_high_u32(err0));
err00 = vpadd_u32(err00, err00);
err = vget_lane_u32(err00, 0);
#endif
if (ret_err > err)
{
ret_err = err;
ret = i;
#if 0 // enable early exit?
if (ret_err == 0)
break;
#endif
}
}
return ret;
}
现在,if (ret_err > err)
显然是数据危害。然后我manually "unrolled" loop by two并修改代码以使用err0和err1并在执行下一轮计算后检查它们。根据剖析器我得到了一些改进。在简单的霓虹灯循环中,我在两行vget_lane_u32
中花费了大约30%的整个函数,后跟if (ret_err > err)
。在我展开两次后,这些操作开始占25%(例如我的整体加速率大约为10%)。另外,检查armv7版本,设置err0(vmov.32 r6, d16[0]
)和访问(cmp r12, r6
)之间只有8条指令。 Ť
注意,在代码提前退出是ifdefed out。启用它会使功能更慢。如果我将其展开四次并更改为使用四个errN
变量并进行两轮deffer检查,那么我仍然在profiler中看到vget_lane_u32
花费了太多时间。当我检查生成的asm时,似乎编译器会破坏所有优化尝试,因为它重用了一些errN
寄存器,这些寄存器有效地使vget_lane_u32
的CPU访问结果比我想要的更早(并且我的目标是延迟访问按10-20指示)。然而,只有当我展开4并将所有四个errN标记为挥发性vget_lane_u32
完全从探测器中的雷达中消失时,if (ret_err > errN)
检查显然变得很慢,因为现在它们可能最终成为常规堆栈变量总体而言,4x手动循环展开的这4项检查开始占40%。看起来有适当的手动操作,它可以使它正常工作:早期循环退出,同时避免霓虹灯 - >手臂档位并在循环中有一些手臂逻辑,但是,需要额外的维护来处理手臂asm使得在大型项目中维护这种代码(没有任何armasm)要复杂10倍。
更新
将数据从霓虹灯移动到手臂寄存器时,此处的示例停止。为了实现早期存在,我需要每个循环从氖到手臂移动一次。根据xcode附带的采样分析器,仅此移动就占整个功能的50%以上。我尝试在mov之前和/或之后添加大量的noops,但似乎没有任何东西影响探查器的结果。我尝试使用vorr d0,d0,d0作为noops:没有区别。什么是失速的原因,或者探查器只是显示错误的结果?