【问题标题】:Fastest way for wrapping a value in an interval在间隔中包装值的最快方法
【发布时间】:2021-11-12 06:28:30
【问题描述】:

我很好奇将浮点值 x 包装在半封闭区间 [0; a[ 中的方法。

例如,我可以有一个任意实数,比如x = 354638.515,我希望将它折叠成[0; 2π[,因为我有一个很好的sin 近似值。

fmod 标准 C 函数在我的基准测试中显示得相当高,通过检查各种 libc 实现的源代码,我可以理解原因:这东西相当分支,可能是为了处理很多IEEE754 特定问题:

就我而言,我只关心通常的实数值,而不是 NaN,也不关心无穷大。我的范围在编译时也是已知的并且是正常的(常见的情况是 π/2),因此不需要检查诸如 range == 0 之类的“特殊情况”。

因此,对于该特定用例,fmod 的良好实现是什么?

【问题讨论】:

  • 你能拿到标准fmod的源代码,并去掉所有的安全检查吗?
  • “表现得相当高”不是一个有意义的陈述。高性能?高时间?成本高?效率高吗?
  • 参数缩减的标准技术是 Payne-Hanek 方法或它的一些子集或变体,具体取决于您的特定需求。一个乘以 1/(2π) 的准备值并取余数(通常通过将商截断为整数并使用 FMA 推导出余数)。可以不使用 FMA 获取残差,而是获取产​​品的分数部分,这与残差的不同之处在于它已按 1/(2π) 缩放,但随后您将应用为此设计的 sin 近似值。
  • 也就是说,1/(2π) 的使用取决于需要。对于简单的近似值,单个 double 值可能就足够了。为了获得更好的准确性,您可能需要某种形式的扩展精度。要覆盖完整的浮点范围,您可能需要一个值表,该表按要减少的值的指数进行索引。然后,当减少的数字如此之大以至于这些位乘以 1/(2π) 中的高位总是整数时,可以省略高位。因此,在一个 Stack Overflow 答案中要涵盖的内容和记录的内容太多。这取决于你的情况。
  • 除非询问两种语言之间的差异或交互,否则不要同时标记 C 和 C++。选择一个并删除另一个标签。

标签: c++ math floating-point vectorization


【解决方案1】:

假设范围是恒定且正的,您可以计算其倒数以避免昂贵的除法。

void fast_fmod(float * restrict dst, const float * restrict src, size_t n, float divisor) {
   float reciprocal = 1.0f / divisor;
   for (size_t i = 0; i < n; ++i)
     dst[i] = src[i] - divisor * (int)(src[i] * reciprocal);
}

最后附上一个简单demo的代码是:

#include <stdlib.h>
#include <stdio.h>
#include <math.h>

void fast_fmod(float * restrict dst, const float * restrict src, size_t n, float divisor) {
   float reciprocal = 1.0f / divisor;

   for (size_t i = 0; i < n; ++i)
     dst[i] = src[i] - divisor * (int)(src[i] * reciprocal);
}

int main() {
    float src[9] = {-4, -3, -2, -1, 0, 1, 2, 3, 4};
    float dst[9];
    float div = 3;

    fast_fmod(dst, src, 9, div);

    for (int i = 0; i < 9; ++i) {
        printf("fmod(%g, %g) = %g vs %g\n", src[i], div, dst[i], fmod(src[i], div));
    }
}

产生预期的输出:

fmod(-4, 3) = -1 vs -1
fmod(-3, 3) = 0 vs -0
fmod(-2, 3) = -2 vs -2
fmod(-1, 3) = -1 vs -1
fmod(0, 3) = 0 vs 0
fmod(1, 3) = 1 vs 1
fmod(2, 3) = 2 vs 2
fmod(3, 3) = 0 vs 0
fmod(4, 3) = 1 vs 1

使用 GCC 编译命令:

$ gcc prog.c -o prog -O3 -march=haswell -lm -fopt-info-vec
prog.c:8:4: optimized: loop vectorized using 32 byte vectors
prog.c:8:4: optimized: loop vectorized using 32 byte vectors
prog.c:8:30: optimized: basic block part vectorized using 32 byte vectors

因此代码被很好地矢量化了。


编辑

看起来 CLANG 在矢量化这段代码方面做得更好:

  401170:   c5 fc 10 24 8e          vmovups (%rsi,%rcx,4),%ymm4
  401175:   c5 fc 10 6c 8e 20       vmovups 0x20(%rsi,%rcx,4),%ymm5
  40117b:   c5 fc 10 74 8e 40       vmovups 0x40(%rsi,%rcx,4),%ymm6
  401181:   c5 fc 10 7c 8e 60       vmovups 0x60(%rsi,%rcx,4),%ymm7
  401187:   c5 6c 59 c4             vmulps %ymm4,%ymm2,%ymm8
  40118b:   c5 6c 59 cd             vmulps %ymm5,%ymm2,%ymm9
  40118f:   c5 6c 59 d6             vmulps %ymm6,%ymm2,%ymm10
  401193:   c5 6c 59 df             vmulps %ymm7,%ymm2,%ymm11
  401197:   c4 41 7e 5b c0          vcvttps2dq %ymm8,%ymm8
  40119c:   c4 41 7e 5b c9          vcvttps2dq %ymm9,%ymm9
  4011a1:   c4 41 7e 5b d2          vcvttps2dq %ymm10,%ymm10
  4011a6:   c4 41 7e 5b db          vcvttps2dq %ymm11,%ymm11
  4011ab:   c4 41 7c 5b c0          vcvtdq2ps %ymm8,%ymm8
  4011b0:   c4 41 7c 5b c9          vcvtdq2ps %ymm9,%ymm9
  4011b5:   c4 41 7c 5b d2          vcvtdq2ps %ymm10,%ymm10
  4011ba:   c4 41 7c 5b db          vcvtdq2ps %ymm11,%ymm11
  4011bf:   c5 3c 59 c3             vmulps %ymm3,%ymm8,%ymm8
  4011c3:   c5 34 59 cb             vmulps %ymm3,%ymm9,%ymm9
  4011c7:   c5 2c 59 d3             vmulps %ymm3,%ymm10,%ymm10
  4011cb:   c5 24 59 db             vmulps %ymm3,%ymm11,%ymm11
  4011cf:   c4 c1 5c 5c e0          vsubps %ymm8,%ymm4,%ymm4
  4011d4:   c4 c1 54 5c e9          vsubps %ymm9,%ymm5,%ymm5
  4011d9:   c4 c1 4c 5c f2          vsubps %ymm10,%ymm6,%ymm6
  4011de:   c4 c1 44 5c fb          vsubps %ymm11,%ymm7,%ymm7
  4011e3:   c5 fc 11 24 8f          vmovups %ymm4,(%rdi,%rcx,4)
  4011e8:   c5 fc 11 6c 8f 20       vmovups %ymm5,0x20(%rdi,%rcx,4)
  4011ee:   c5 fc 11 74 8f 40       vmovups %ymm6,0x40(%rdi,%rcx,4)
  4011f4:   c5 fc 11 7c 8f 60       vmovups %ymm7,0x60(%rdi,%rcx,4)
  4011fa:   48 83 c1 20             add    $0x20,%rcx
  4011fe:   48 39 c8                cmp    %rcx,%rax
  401201:   0f 85 69 ff ff ff       jne    401170 <fast_fmod+0x40>

【讨论】:

  • 对于src[i] - divisor * (int)(src[i] * reciprocal);,通常最好使用fma。并且可以使用trunc 而不是转换为int,以避免更改类型。 (编译器可能会优化这些,尤其是后者,但它可能不会,并且更接近所需的操作是更可取的。另外,到int 的转换可能会溢出。)
  • @EricPostpischil,是的,我有点惊讶编译器没有注意到srs[i] - divisor * X 可以用fma 指令处理
  • fma(a, b, c)a*b+c 在语义上是不同的,因为后者名义上包括前者没有的舍入,并且编译器是否省略(C 和 C++ 标准允许)取决于设置。
  • @EricPostpischil,trunc GCC 无法向量化循环。看起来一个好的解决方案需要手写的内在知识。
  • @EricPostpischil,你是对的。使用-ffast-math 选项,CLANG 能够使用vfnmadd213ps 指令。
猜你喜欢
  • 2016-03-18
  • 2014-01-22
  • 2018-06-14
  • 1970-01-01
  • 1970-01-01
  • 2010-12-07
  • 1970-01-01
  • 2012-05-22
  • 2011-11-14
相关资源
最近更新 更多