夹紧真正的最快方式(固定/浮点)值?(Fastest way to clamp a real (fi

2019-10-22 06:02发布

是否有夹实数比使用if语句或三元运营商更有效的方法? 我想无论是双打和32位固定点实现(16.16)做到这一点。 我要求的代码,可以处理这两种情况; 他们将在不同的功能来处理。

很显然,我可以这样做:

double clampedA;
double a = calculate();
clampedA = a > MY_MAX ? MY_MAX : a;
clampedA = a < MY_MIN ? MY_MIN : a;

要么

double a = calculate();
double clampedA = a;
if(clampedA > MY_MAX)
    clampedA = MY_MAX;
else if(clampedA < MY_MIN)
    clampedA = MY_MIN;

固定点的版本将使用函数/宏比较。

这是在代码的性能关键部分做了,所以我在寻找有效率的方式做到这一点越好(我怀疑将涉及的位操作)

编辑:它必须是标准/便携式C,特定于平台的功能是任何兴趣不在这里。 此外, MY_MINMY_MAX是作为值I要夹紧的(在上面的例子中双打)相同的类型。

Answer 1:

对于16.16表示,简单的三元不太可能被做得更好速度明智的。

而对于双打,因为你需要它的标准/便携式C,任何种类的位小提琴演奏会结束得很惨。

即使位小提琴是可能的(我怀疑),你会依靠双打的二进制表示。 这一点(和它们的大小)是依赖于实现。

也许你可以“猜”这个用的sizeof(双),然后比较反对共同二进制表示的各种双值的布局,但我认为你是一个隐藏不了了之。

最好的规则是告诉编译器你想要什么(即三元),并让它优化你。

编辑:赔礼道歉时间。 我只是测试quinmars想法(如下图),和它的作品 - 如果你有IEEE-754浮点数。 这给了下面的代码的约20%的速度提升。 IObviously不可移植的,但我觉得有可能会问你的编译器,如果它使用IEEE754浮点格式与#IF标准的方式...?

  double FMIN = 3.13;
  double FMAX = 300.44;

  double FVAL[10] = {-100, 0.23, 1.24, 3.00, 3.5, 30.5, 50 ,100.22 ,200.22, 30000};
  uint64  Lfmin = *(uint64 *)&FMIN;
  uint64  Lfmax = *(uint64 *)&FMAX;

    DWORD start = GetTickCount();

    for (int j=0; j<10000000; ++j)
    {
        uint64 * pfvalue = (uint64 *)&FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < Lfmin) ? Lfmin : (*pfvalue > Lfmax) ? Lfmax : *pfvalue;
    }

    volatile DWORD hacktime = GetTickCount() - start;

    for (int j=0; j<10000000; ++j)
    {
        double * pfvalue = &FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < FMIN) ? FMIN : (*pfvalue > FMAX) ? FMAX : *pfvalue;
    }

    volatile DWORD normaltime = GetTickCount() - (start + hacktime);


Answer 2:

老问题,但我工作的今天,这一问题(与双打/花车)。

最好的办法是使用SSE MINSS / MAXSS的花车和SSE2 MINSD / MAXSD双打。 这些都是网点,并采取每一个时钟周期,并且易于使用多亏了编译器内在。 他们赋予比用的std ::最小/最大钳位相比,其性能提高了一个数量级以上。

您可能会感到惊奇。 我当然没有! 不幸的是VC ++ 2010使用简单比较的标准::最小/最大即便/弓:SSE2和/ FP:快速启用。 我不能为其他编译器说话。

下面是必要的代码在VC ++做到这一点:

#include <mmintrin.h>

float minss ( float a, float b )
{
    // Branchless SSE min.
    _mm_store_ss( &a, _mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float maxss ( float a, float b )
{
    // Branchless SSE max.
    _mm_store_ss( &a, _mm_max_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float clamp ( float val, float minval, float maxval )
{
    // Branchless SSE clamp.
    // return minss( maxss(val,minval), maxval );

    _mm_store_ss( &val, _mm_min_ss( _mm_max_ss(_mm_set_ss(val),_mm_set_ss(minval)), _mm_set_ss(maxval) ) );
    return val;
}

双精度代码是除与xxx_sd代替相同。

编辑:最初我写的钳位功能评价。 但看汇编输出。我注意到,VC ++编译器是不是足够聪明,剔除冗余的举动。 少了一个指令。 :)



Answer 3:

GCC和铛生成以下简单的,直接的,可移植的代码美丽的装配:

double clamp(double d, double min, double max) {
  const double t = d < min ? min : d;
  return t > max ? max : t;
}

> gcc -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

GCC生成的组件:

maxsd   %xmm0, %xmm1    # d, min
movapd  %xmm2, %xmm0    # max, max
minsd   %xmm1, %xmm0    # min, max
ret

> clang -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

铛生成的组件:

maxsd   %xmm0, %xmm1
minsd   %xmm1, %xmm2
movaps  %xmm2, %xmm0
ret

三条指令(不包括RET),别无分店。 优秀的。

将其用4.7 GCC在Ubuntu 13.04测试和铛3.2与Core i3的中号350在一个侧面说明中,简单的C ++代码调用的std ::分钟和std ::最大产生的相同组件。

这是双打。 而对于INT,GCC和铛生成汇编与五个指令(不包括RET)和没有分支。 也很好。

我目前不使用固定点,所以我不会放弃固定点意见。



Answer 4:

如果您的处理器具有绝对值的快速指令(如x86的一样),你可以做一个网点的最小值和最大值,这将是比快if语句或三元操作。

min(a,b) = (a + b - abs(a-b)) / 2
max(a,b) = (a + b + abs(a-b)) / 2

如果条件之一是零(因为往往当你夹紧的情况下)的代码,进一步简化了一下:

max(a,0) = (a + abs(a)) / 2

当你结合这两个操作就可以更换两节/2到单个/4*0.25节省了一步。

下面的代码是3倍以上比三元快上我的速龙II X2,使用FMIN = 0优化时。

double clamp(double value)
{
    double temp = value + FMAX - abs(value-FMAX);
#if FMIN == 0
    return (temp + abs(temp)) * 0.25;
#else
    return (temp + (2.0*FMIN) + abs(temp-(2.0*FMIN))) * 0.25;
#endif
}


Answer 5:

三元运算符是真的要走的路,因为大多数编译器能够编译成一个使用条件移动,而不是分公司的(因而避免了错误预测的处罚和管道气泡等)的本地硬件的操作。 位操作很可能导致负载命中商店

具体而言,PPC和x86与SSE2有一个可以表示为一个内在的东西像这样的硬件运算:

double fsel( double a, double b, double c ) {
  return a >= 0 ? b : c; 
}

其优点是,它这个管道内,而不会造成一个分支。 事实上,如果你的编译器使用的内在,你可以用它来直接实现您的钳:

inline double clamp ( double a, double min, double max ) 
{
   a = fsel( a - min , a, min );
   return fsel( a - max, max, a );
}

我强烈建议您不要使用整数运算双打的位操作 。 在大多数现代CPU还有比通过采取往返于dcache中移动其他双和int寄存器之间的数据没有直接的手段。 这将导致数据的危险称为负载命中-Store基本上清空出CPU管线直到存储器中的写入已经完成(通常大约40个循环左右)。

唯一的例外是,如果双精度值已经在内存中,而不是在寄存器中:在这种情况下,没有负载命中店的危险。 但是你的例子表明你刚刚计算出的双和,这意味着它很可能仍处于XMM1函数返回它。



Answer 6:

IEEE 754浮点的位进行排序的方式,如果你比较解释为一个整数位,你会得到相同的结果,如果直接漂浮你对它们进行比较。 所以,如果你发现或者知道一种夹,你可以使用它的整数(IEEE 754)浮也。 对不起,我不知道一个更快的方法。

如果您有存储在阵列中的彩车你可以考虑使用一些CPU的扩展名如SSE3,作为rkj说。 你可以看看liboil它所有肮脏的工作适合你。 保持你的程序移植,如果可能使用了速度更快的CPU指令。 (我不知道寿OS /编译器无关liboil如何)。



Answer 7:

而不是测试和分支,我通常使用这种格式用于夹紧:

clampedA = fmin(fmax(a,MY_MIN),MY_MAX);

虽然我从来没有做过的编译代码的任何性能分析。



Answer 8:

实际上,没有像样的编译器将作出if()语句和之间的区别?:表达式。 该代码是很简单的,他们将能够发现可能的路径。 这就是说,你的两个例子是不相同的。 使用等效代码:?会

a = (a > MAX) ? MAX : ((a < MIN) ? MIN : a);

作为避免A <MIN测试时> MAX。 现在,可以有所作为的,因为编译器,否则将有被发现两次测试之间的关系。

如果夹紧是罕见的,你可以测试需要有一个测试钳:

if (abs(a - (MAX+MIN)/2) > ((MAX-MIN)/2)) ...

例如用MIN = 6和MAX = 10,这将首先由8位移的向下,然后检查它是否处在-2和+2之间。 这是否可以节省任何依赖于分支的相对成本很多。



Answer 9:

这里有类似可能更快地实现@罗迪的回答 :

typedef int64_t i_t;
typedef double  f_t;

static inline
i_t i_tmin(i_t x, i_t y) {
  return (y + ((x - y) & -(x < y))); // min(x, y)
}

static inline
i_t i_tmax(i_t x, i_t y) {
  return (x - ((x - y) & -(x < y))); // max(x, y)
}

f_t clip_f_t(f_t f, f_t fmin, f_t fmax)
{
#ifndef TERNARY
  assert(sizeof(i_t) == sizeof(f_t));
  //assert(not (fmin < 0 and (f < 0 or is_negative_zero(f))));
  //XXX assume IEEE-754 compliant system (lexicographically ordered floats)
  //XXX break strict-aliasing rules
  const i_t imin = *(i_t*)&fmin;
  const i_t imax = *(i_t*)&fmax;
  const i_t i    = *(i_t*)&f;
  const i_t iclipped = i_tmin(imax, i_tmax(i, imin));

#ifndef INT_TERNARY
  return *(f_t *)&iclipped;
#else /* INT_TERNARY */
  return i < imin ? fmin : (i > imax ? fmax : f); 
#endif /* INT_TERNARY */

#else /* TERNARY */
  return fmin > f ? fmin : (fmax < f ? fmax : f);
#endif /* TERNARY */
}

见计算两个整数的最小值(MIN)或最高(最大)没有分支和比较浮点数

在IEEE浮点和双精度格式被设计成使得数字“字典顺序”,它 - 在IEEE建筑师威廉Kahan的的话的意思是“如果以相同的格式2浮点数是有序的(比如说X <Y),则他们是有序以同样的方式时,他们的位被重新解释为符号 - 幅度整数“。

测试程序:

/** gcc -std=c99 -fno-strict-aliasing -O2 -lm -Wall *.c -o clip_double && clip_double */
#include <assert.h> 
#include <iso646.h>  // not, and
#include <math.h>    // isnan()
#include <stdbool.h> // bool
#include <stdint.h>  // int64_t
#include <stdio.h>

static 
bool is_negative_zero(f_t x) 
{
  return x == 0 and 1/x < 0;
}

static inline 
f_t range(f_t low, f_t f, f_t hi) 
{
  return fmax(low, fmin(f, hi));
}

static const f_t END = 0./0.;

#define TOSTR(f, fmin, fmax, ff) ((f) == (fmin) ? "min" :       \
                  ((f) == (fmax) ? "max" :      \
                   (is_negative_zero(ff) ? "-0.":   \
                    ((f) == (ff) ? "f" : #f))))

static int test(f_t p[], f_t fmin, f_t fmax, f_t (*fun)(f_t, f_t, f_t)) 
{
  assert(isnan(END));
  int failed_count = 0;
  for ( ; ; ++p) {
    const f_t clipped  = fun(*p, fmin, fmax), expected = range(fmin, *p, fmax);
    if(clipped != expected and not (isnan(clipped) and isnan(expected))) {
      failed_count++;
      fprintf(stderr, "error: got: %s, expected: %s\t(min=%g, max=%g, f=%g)\n", 
          TOSTR(clipped,  fmin, fmax, *p), 
          TOSTR(expected, fmin, fmax, *p), fmin, fmax, *p);
    }
    if (isnan(*p))
      break;
  }
  return failed_count;
}  

int main(void)
{
  int failed_count = 0;
  f_t arr[] = { -0., -1./0., 0., 1./0., 1., -1., 2, 
        2.1, -2.1, -0.1, END};
  f_t minmax[][2] = { -1, 1,  // min, max
               0, 2, };

  for (int i = 0; i < (sizeof(minmax) / sizeof(*minmax)); ++i) 
    failed_count += test(arr, minmax[i][0], minmax[i][1], clip_f_t);      

  return failed_count & 0xFF;
}

在控制台:

$ gcc -std=c99 -fno-strict-aliasing -O2 -lm *.c -o clip_double && ./clip_double 

它打印:

error: got: min, expected: -0.  (min=-1, max=1, f=0)
error: got: f, expected: min    (min=-1, max=1, f=-1.#INF)
error: got: f, expected: min    (min=-1, max=1, f=-2.1)
error: got: min, expected: f    (min=-1, max=1, f=-0.1)


Answer 10:

我尝试了SSE处理这一自己,并汇编输出看上去颇有几分清洁,所以我起初感到鼓舞,但它的时序上千次后,它实际上是相当慢一点。 它确实看起来像VC ++编译器是不是足够聪明,知道什么是你真正打算,并且它似乎当它不应该的XMM寄存器和存储器之间移动的东西来回。 这就是说,我不知道为什么编译器是不是足够聪明的使用在三元运算符的SSE最小/最大指令时,它似乎使用SSE指令对所有浮点计算反正。 在另一方面,如果你正在编译为PowerPC,您可以使用FSEL的FP寄存器内在的,并且它的方式更快。



Answer 11:

如果我理解正确的,要限制的值“a”到MY_MIN和的my_max之间的范围。 该类型的“a”是一个双。 您没有指定MY_MIN或的my_max的类型。

简单的表达:

clampedA = (a > MY_MAX)? MY_MAX : (a < MY_MIN)? MY_MIN : a;

应该做的伎俩。

我觉得有可能是被做了一个小的优化,如果的my_max和MY_MIN正好是整数:

int b = (int)a;
clampedA = (b > MY_MAX)? (double)MY_MAX : (b < MY_MIN)? (double)MY_MIN : a;

通过改变到整数比较,有可能你可能会得到一个轻微的速度优势。



Answer 12:

如果要使用快速绝对值指令,看看这个文档片断的代码,我在发现小型机 ,它夹一个浮动区间[0,1]

clamped = 0.5*(fabs(x)-fabs(x-1.0f) + 1.0f);

(I简化了码的位)。 我们可以考虑一下为采用两个值,一是体现为> 0

fabs(x)

而另一反射约1.0至是<1.0

1.0-fabs(x-1.0)

我们把他们的平均值。 如果是在范围内,那么这两个值是相同为x,那么他们平均将再次为x。 如果超出范围,则该值之一是x,另一个是x翻转“边界”的观点,所以他们平均会精确的边界点。



Answer 13:

如上所指出的,FMIN / fmax的功能很好地工作(在GCC,与-ffast-数学)。 虽然gfortran具有图案使用对应于最大/最小值,克++不IA指令。 在国际刑事法院必须改用标准::最小/最大,因为ICC不允许短切的规格如何与非有限操作数FMIN / FMAX工作。



Answer 14:

我的2美分C ++。 可能不是任何不同于使用三元运营商,希望不产生分支代码

template <typename T>
inline T clamp(T val, T lo, T hi) {
    return std::max(lo, std::min(hi, val));
}


文章来源: Fastest way to clamp a real (fixed/floating point) value?