[英]C/C++ fast absolute difference between two series
我有兴趣生成高效的 c/c++ 代码来获取两个时间序列之间的差异。 更精确:时间序列值存储为 uint16_t arrays,长度固定且等长 == 128。
我很擅长纯 c 以及纯 c++ 实现。 我的代码示例在 c++
我的意图是:
Let A,B and C be discrete time series of length l with a value-type of uint16_t.
Vn[n<l]: Cn = |An - Bn|;
我能想到的伪代码:
for index i:
if a[i] > b[i]:
c[i] = a[i] - b[i]
else:
c[i] = b[i] - a[i]
或者在 c/c++ 中
for(uint8_t idx = 0; idx < 128; idx++){
c[i] = a[i] > b[i] ? a[i] - b[i] : b[i] - a[i];
}
但我真的不喜欢循环中的 if/else 语句。 我可以接受循环——这可以由编译器展开。 有点像:
void getBufDiff(const uint16_t (&a)[], const uint16_t (&b)[], uint16_t (&c)[]) {
#pragma unroll 16
for (uint8_t i = 0; i < 128; i++) {
c[i] = a[i] > b[i] ? a[i] - b[i] : b[i] - a[i];
}
#end pragma
}
我正在寻找的是一个“魔术代码”,它可以加速 if/else 并让我得到两个无符号值之间的绝对差值。
我可以接受 +/- 1 的精度(以防发生一些位魔术)。 我也同意更改数据类型以获得更快的结果。 而且我也可以放弃其他东西的循环。
所以像:
void getBufDiff(const uint16_t (&a)[], const uint16_t (&b)[], uint16_t (&c)[]) {
#pragma unroll 16
for (uint8_t i = 0; i < 128; i++) {
c[i] = magic_code_for_abs_diff(a[i],b[i]);
}
#end pragma
}
尝试对这两个值进行异或运算。 仅针对其中一种情况给出正确的结果。
编辑 2:
在我的笔记本电脑上对不同的方法进行了快速测试。
对于 250000000 个条目,这是性能(256 轮):
c[i] = a[i] > b[i] ? a[i] - b[i] : b[i] - a[i]; ~500ms
c[i] = std::abs(a[i] - b[i]); ~800ms
c[i] = ((a[i] - b[i]) + ((a[i] - b[i]) >> 15)) ^ (i >> 15) ~425ms
uint16_t tmp = (a[i] - b[i]); c[i] = tmp * ((tmp > 0) - (tmp < 0)); ~600ms
uint16_t ret[2] = { a[i] - b[i], b[i] - a[i] };c[i] = ret[a[i] < b[i]] ~900ms
c[i] = ((a[i] - b[i]) >> 31 | 1) * (a[i] - b[i]); ~375ms
c[i] = ((a[i] - b[i])) ^ ((a[i] - b[i]) >> 15) ~425ms
您的问题很适合 SIMD。 GCC 可以自动完成,这里是一个简化的例子: https://godbolt.org/z/36nM8bYYv
void absDiff(const uint16_t* a, const uint16_t* b, uint16_t* __restrict__ c)
{
for (uint8_t i = 0; i < 16; i++)
c[i] = a[i] - b[i];
}
请注意,我添加了__restrict__
以启用自动矢量化,否则编译器必须假设您的 arrays 可能重叠并且使用 SIMD 是不安全的(因为某些写入可能会改变循环中的未来读取)。
我一次将其简化为 16 个,并为了便于说明删除了绝对值。 生成的程序集是:
vld1.16 {q9}, [r0]!
vld1.16 {q11}, [r1]!
vld1.16 {q8}, [r0]
vld1.16 {q10}, [r1]
vsub.i16 q9, q9, q11
vsub.i16 q8, q8, q10
vst1.16 {q9}, [r2]!
vst1.16 {q8}, [r2]
bx lr
这意味着它一次从a
加载 8 个整数,然后从b
加载,重复一次,然后一次执行 8 个减法,然后再次将 8 个值存储两次到c
中。 比没有 SIMD 的指令少很多。
当然,它需要进行基准测试以查看这在您的系统上是否真的更快(在您加回绝对值部分后,我建议使用您的?:
方法,它不会破坏自动矢量化),但我希望它会快得多。
快速
abs
(在两个补整数下)可以实现为(x + (x >> N)) ^ (x >> N)
其中 N 是 int - 1 的大小,即在您的情况下为 15。 这是std::abs
的可能实现。 你还是可以试试
–怪异的回答
由于您写的是“我可以使用 +/- 1 精度”,因此您可以使用 XOR 解决方案:代替abs(x)
,执行x ^ (x >> 15)
。 对于负值,这将给出 off-by-1 结果。
如果您想计算负值的正确结果,请使用其他答案( x >> 15
校正)。
无论如何,这种 XOR 技巧只有在不可能发生溢出时才有效。 因此,编译器无法用使用 XOR 的代码替换abs
。
尝试让编译器看到 SIMD 指令的条件通道选择模式,如下所示(伪代码):
// store a,b to SIMD registers
for(0 to 32)
a[...] = input[...]
b[...] = input2[...]
// single type operation, easily parallelizable
for(0 to 32)
vector1[...] = a[...] - b[...]
// single type operation, easily parallelizable
// maybe better to compute b-a to decrease dependency to first step
// since a and b are already in SIMD registers
for(0 to 32)
vector2[...] = -vector1[...]
// single type operation, easily parallelizable
// re-use a,b registers, again
for(0 to 32)
vector3[...] = a[...] < b[...]
// x84 architecture has SIMD instructions for this
// operation is simple, no other calculations inside, just 3 inputs, 1 out
// all operands are registers (at least should be, if compiler works fine)
for(0 to 32)
vector4[...] = vector3[...] ? vector2[...]:vetor1[...]
如果您编写基准代码,我可以将其与其他解决方案进行比较。 但是,对于有问题的第一个基准代码自动执行相同操作的好的编译器(或好的编译器标志)并不重要。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.