【问题标题】:Optimization Headache - removing if's from Look Up Table优化头痛 - 从查找表中删除 if
【发布时间】:2011-11-15 08:51:14
【问题描述】:

我正在尝试优化以下代码,这是我的应用程序的瓶颈。 它的作用:它采用双值 value1 和 value2 并尝试找到包括校正因子在内的最大值。如果两个值之间的差异大于 5.0(LUT 被缩放 10 倍),我可以取这两个值的最大值。如果差值小于 5.0,我可以使用 LUT 中的校正因子。

有谁知道这段代码有什么更好的风格?我不知道我在哪里浪费时间 - 是大量的 if 还是乘以 10?

double value1, value2;
// Lookup Table scaled by 10 for (ln(1+exp(-abs(x)))), which is almost 0 for x > 5 and symmetrical around 0. LUT[0] is x=0.0, LUT[40] is x=4.0.
const logValue LUT[50] = { ... }

if (value1 > value2)
{
    if (value1 - value2 >= 5.0)
    {
        return value1;
    }
    else
    {
        return value1 + LUT[(uint8)((value1 - value2) * 10)];
    }
}
else
{
    if (value2 - value1 >= 5.0)
    {
        return value2;
    }
    else
    {
        return value2 + LUT[(uint8)((value2 - value1) * 10)];
    }
}

【问题讨论】:

  • 既然你已经知道了结果,为什么还要计算max()
  • 哇...谢谢,那真是太愚蠢了...纠正了!
  • 如果这是一个真正的瓶颈,GPU 非常擅长这个(饱和度加法和查找表)。
  • 与其将查找表截断为 50,不如考虑将其扩展到最大可能值。使用扩展的 LUT,您可以摆脱 if 测试和分支。内存通常很便宜,而分支通常不是。
  • @David:内存没那么便宜;它将东西推出缓存。我更倾向于使用非线性键来缩小 LUT。不过,我必须绘制 LUT 才能找到便宜的密钥转换。

标签: c++ arrays lookup


【解决方案1】:

使用 Excel 几分钟会生成一个近似方程,看起来它可能具有您需要的精度,因此您可以完全取消查找表。您仍然需要一个条件来确保方程的参数保持在优化的范围内。

double diff = abs(value1 - value2);
double dmax = (value1 + value2 + diff) * 0.5; // same as (min+max+(max-min))/2
if (diff > 5.0)
    return dmax;
return dmax + 4.473865638/(2.611112371+diff) + 0.088190879*diff + -1.015046114;

附:我不保证这会更快,只是它是一种足够不同的方法,值得进行基准测试。

附言可以更改约束以得出略有不同的常数,有很多变化。这是我做的另一组,你的表格和公式之间的差异总是小于 0.008,而且每个值都小于前面的值。

return dmax + 3.441318133/(2.296924445+diff) + 0.065529678*diff + -0.797081529;

编辑:我测试了这段代码(第二个公式),通过 100 次对 0 到 10 之间的一百万个随机数,以及来自问题的原始代码 MSalters currently accepted answer 和蛮力实施max(value1,value2)+log(1.0+exp(-abs(value1-value2)))。我在双核 AMD Athlon 和 Intel 四核 i7 上试了一下,结果大致一致。这是一个典型的运行:

  • 原始:1.32 秒。
  • MSalters:1.13 秒。
  • 我的:0.67 秒。
  • 蛮力:4.50 秒。

这些年来,处理器的速度快得令人难以置信,现在它们可以进行几次浮点乘法和除法运算,速度比在内存中查找值的速度还要快。这种方法不仅在现代 x86 上更快,而且更准确;方程中的近似误差远小于截断查找输入所导致的步长误差。

自然结果仍会因您的处理器和编译器而异;您自己的特定目标仍然需要进行基准测试。

【讨论】:

  • +1 我不保证这会更快,只是它是一种不同的方法,值得进行基准测试。
  • 如果您可以摆脱该分支,则函数逼近方法会更好。不幸的是,不知道 abs(value1-value2) 的范围,我们只有 0 到 5,而且你被分支卡住了。
  • @David Hammen,如果处理器具有无分支布尔值,您可以将公式乘以 (diff<5.0),但我不确定它会更快。近似公式的范围也可以以一定的准确性为代价来扩大。
  • @Mark Ransom:是的,我玩过一些有理函数近似。 (我之前使用过log(exp(x)+exp(y)) 的有理近似值。)了解近似值的范围确实会有所帮助。或乘以(diff<5.0)。但这在计算方面也非常昂贵。
  • 这个准确度应该也够了——我明天试试!
【解决方案2】:

它可能会在两条路径上同等地下降,从而导致处理器出现大量流水线问题。

您是否尝试过分析?

我还建议尝试使用标准库,看看是否有帮助(例如,它是否能够使用特定于处理器的指令):

double diff = std::fabs(value1 - value2);
double maxv = std::max(value1, value2);
return (diff >= 5.0) ? maxv : maxv + LUT[(uint8)((diff) * 10)];

【讨论】:

  • 这看起来很简单,但在计算上却并非如此。你有三个分支;两个恰好被隐藏了。当涉及到性能关键代码时,分支越少越好。
  • +1 虽然它可能只是表面上看起来更简单,但它确实也更简洁地传达了目的和意图。如果处理器具有处理 fab 和后续舍入的智能指令,它将应用它(即使它需要在 std::fabs 中使用特殊的内在函数)。但是,当通过手动分支从编译器中“隐藏”fabs 时,所有的赌注都被取消了。 (我知道编译器能够积极地发现这种优化;当你告诉编译器你需要什么而不是如何时,他们这样做客观上更容易)跨度>
  • fabs 不只是简单的位清除,至少在 IEEE754 中是这样吗?此外,对于一个体面的编译器,它无论如何都是一个分支:计算value1 - value2,现在设置了符号标志,在其上分支,{交换 value1 和 value2 if sign set}。 SSA 应该使这种优化变得相当轻松。
  • 编译器很聪明,但没那么聪明。至少 gcc 和 clang 不是。使用 g++ -O3,此实现比利用两个数字的最大值与数字之间的绝对差之间的关系的实现慢约 20%。使用 clang 时,更长的方法的优势下降到 5% 左右,但它仍然存在。
  • @David Hammen 您能否提供一个指向您的测试挂钩(以及您所指的特殊实现)的链接?
【解决方案3】:

我可能会编写一些不同的代码来处理value2<value1 案例:

if (value2 < value1) std::swap(value1, value2);
assert(value1 <= value2); // Assertion corrected
int diff = int((value2 - value1) * 10.0);
if (diff >= 50) diff = 49; // Integer comparison iso floating point
return value2 + LUT[diff];

【讨论】:

  • @Mark:完全是我的观点。杀死assert。此外,LUT[49] 不为零,因此这不能满足问题的要求。
  • @David Hammen,您可以随时向 LUT 添加另一个元素并将其设置为零,然后将上述值更改为 51/50。
【解决方案4】:

我将假设在调用该函数时,您很可能会得到必须使用查找表的部分,而不是 &gt;=5.0 部分。在这种情况下,最好引导编译器这样做。

double maxval = value1;
double difference_scaled = (value1-value2)*10;
if (difference < 0)
{
    difference = -difference;
    maxval = value2;
}
if (difference < 50)
    return maxval+LUT[(int)difference_scaled];
else
    return maxval;

试试这个,如果这能提高你的程序的性能,请告诉我。

【讨论】:

  • 看看宏likely也。我不确定它是标准的还是仅在 g++ 中,但如果您的编译器可以使用它,您可以编写 if (likely(difference &lt; 50)) 以便编译器更好地优化。
  • likely 是一个 g++ 的东西。许多其他编译器支持 Profile-Guided Optimzation,它们将从实际程序运行中收集相同的信息。
【解决方案5】:

此代码成为应用程序瓶颈的唯一原因是您多次调用它。你确定你需要吗?也许代码中较高的算法可以更改为使用比较少?

【讨论】:

  • 很遗憾,由于逻辑限制,无法减少对该函数的高层调用。
  • 如果逻辑需要多次调用函数,你可以缓存结果,但这可能不适用于你的情况。
  • @MSalters:是的,但是,它仍然是一个瓶颈。他可能想要缓存应用了函数的对象的结果(函数之外)。
【解决方案6】:

我已经进行了一些非常快速的测试,但请自行分析代码以验证效果。

LUT[] 更改为静态变量让我的速度提高了 600%(从 3.5 秒到 0.6 秒)。这接近我使用的绝对最小值(0.4s)。看看这是否有效并重新分析以确定是否需要进一步优化。

作为参考,我只是在 VC++ 2010 中计时了这个循环的执行时间(内部循环的 1 亿次迭代):

int Counter = 0;

for (double j = 0; j < 10; j += 0.001)
    {
        for (double i = 0; i < 10; i += 0.001)
        {
            ++Counter;
            Value1 += TestFunc1(i, j);
        }
    }

【讨论】:

  • 您认为为什么会这样(静态加速成本)?
  • 根据反汇编输出(在调试版本中)LUT[] 在不是静态时在每个函数调用上重建。您基本上会为一个数组分配 50 个作业,这当然会影响性能。我可能假设当定义为const 时,编译器会在发布版本中对其进行优化,但看起来并非如此。请注意,将LUT[] 移出函数对提高性能具有相同的效果。
  • 看来这个问题:stackoverflow.com/questions/1334069/… 解释了存储在堆栈上的函数中常量数组的这种行为。
【解决方案7】:

您在函数中计算了很多次value1 - value2。只做一次。

转换为uint8_t 也可能有问题。就性能而言,用作从双精度数转换为整数的最佳整数类型是int,使用数组索引的最佳整数类型是int

max_value = value1;
diff = value1 - value2;
if (diff < 0.0) {
  max_value = value2;
  diff = -diff;
}

if (diff >= 5.0) {
  return max_value;
}
else {
  return max_value + LUT[(int)(diff * 10.0)];
}

请注意,上述保证 LUT 索引将介于 0(含)和 50(不含)之间。这里不需要uint8_t

编辑
在尝试了一些变化之后,这是一个相当快的基于 LUT 的近似 log(exp(value1)+exp(value2))

#include <stdint.h>

// intptr_t *happens* to be fastest on my machine. YMMV.
typedef intptr_t IndexType;

double log_sum_exp (double value1, double value2, double *LUT) {
  double diff = value1 - value2;
  if (diff < 0.0) {
    value1 = value2;
    diff = -diff;
  }   
  IndexType idx = diff * 10.0;
  if (idx < 50) {
    value1 += LUT[idx];
  }   
  return value1;
}   

整数类型IndexType 是加快速度的关键之一。我用 clang 和 g++ 进行了测试,两者都表明转换为 intptr_t(我的计算机上的 long)并使用 intptr_t 作为 LUT 的索引比其他整数类型更快。它比某些类型快得多。例如,unsigned long longuint8_t 在我的计算机上是非常糟糕的选择

类型不仅仅是一个提示,至少在我使用的编译器中是这样。这些编译器完全按照代码告诉它从浮点类型到整数类型的转换执行了该操作,而不管优化级别如何。

将整数类型与 50 进行比较而不是将浮点类型与 5.0 进行比较会产生另一个减速带。

最后一个减速带:并非所有编译器都是平等的。 在我的计算机上 (YMMV),g++ -O3 生成的代码比clang -O3 生成的代码慢得多(这个问题慢了25%!),而clang -O3 生成的代码又比由clang -O4.

我也玩过有理函数逼近方法(类似于 Mark Ransom 的回答),但上面显然没有使用这种方法。

【讨论】:

  • +0.5 的想法(使用 int)但我强烈怀疑实际上编译器会生成相同的代码;简介,简介,简介
  • 任何体面的编译器都会执行 CSE 并缓存 value1 - value2 而不是重新计算它。
  • 通用子表达式消除 (CSE) 是一种常见的优化(双关语)
  • @sehe:或者看一下生成的汇编代码。 g++ 和 clang 都会为不同的强制转换生成不同的代码。哪个更快?个人资料,个人资料,个人资料。
猜你喜欢
  • 2011-06-23
  • 2022-10-29
  • 2021-08-31
  • 2020-02-16
  • 2022-12-12
  • 2012-06-20
  • 1970-01-01
  • 2010-09-22
  • 2015-03-28
相关资源
最近更新 更多