簡體   English   中英

這個浮點平方根逼近是如何工作的?

[英]How does this float square root approximation work?

我找到了一個相當奇怪但工作平方根近似的float ; 我真的不明白。 有人能解釋一下為什么這段代碼有效嗎?

float sqrt(float f)
{
    const int result = 0x1fbb4000 + (*(int*)&f >> 1);
    return *(float*)&result;   
}

我測試了一下, 它將std::sqrt()的值輸出大約1到3% 我知道Quake III的快速反平方根 ,我想這里有類似的東西(沒有牛頓迭代),但我真的很感激它的工作原理

(nota:我已經用標記了它,因為它既有效-ish(見注釋)C和C ++代碼)

(*(int*)&f >> 1)右移f的按位表示。 幾乎將指數除以2,這大約相當於取平方根。 1

為何幾乎 在IEEE-754中,實際指數是e-127 2要將此除以2,我們需要e / 2 - 64 ,但上述近似值僅給出e / 2 - 127 所以我們需要在結果指數上加上63。 這是由該魔術常量( 0x1fbb4000 )的位30-23貢獻的。

我想是已經選擇了魔術常數的剩余部分來最小化尾數范圍內的最大誤差,或類似的東西。 然而,尚不清楚它是通過分析,迭代還是啟發式確定的。


值得指出的是,這種方法有點不便攜。 它(至少)做出以下假設:

  • 該平台使用單精度IEEE-754進行float
  • float表示的字節順序。
  • 由於這種方法違反了C / C ++的嚴格別名規則,因此您不會受到未定義行為的影響。

因此,應該避免它,除非你確定它在你的平台上提供了可預測的行為(事實上,它提供了一個有用的加速比sqrtf !)。


1. sqrt(a ^ b)=(a ^ b)^ 0.5 = a ^(b / 2)

2.參見https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Exponent_encoding

見為什么這幾乎工程奧利弗查爾斯沃思解釋。 我正在解決評論中提出的問題。

由於有幾個人已經指出了它的不可移植性,這里有一些方法可以使它更具可移植性,或者至少讓編譯器告訴你它是否不起作用。

首先,C ++允許您在編譯時檢查std::numeric_limits<float>::is_iec559 ,例如在static_assert 您還可以檢查sizeof(int) == sizeof(float) ,如果int是64位,則不會為true,但您真正想要的是使用uint32_t ,如果它存在則總是正好是32位寬,如果您的奇怪架構沒有這樣的整數類型,將會有明確定義的帶有移位和溢出的行為,並會導致編譯錯誤。 無論哪種方式,您還應該static_assert()表示類型具有相同的大小。 靜態斷言沒有運行時成本,如果可能的話,您應該始終以這種方式檢查前提條件。

不幸的是,是否將float的位轉換為uint32_t和移位的測試是big-endian,little-endian或者都不能計算為編譯時常量表達式。 在這里,我將運行時檢查放在依賴於它的代碼部分,但您可能希望將其置於初始化中並執行一次。 實際上,gcc和clang都可以在編譯時優化此測試。

你不想使用不安全的指針轉換,並且我在現實世界中有一些系統可能會因為總線錯誤而導致程序崩潰。 轉換對象表示的最大可移植方式是使用memcpy() 在下面的示例中,我使用union類型化處理,它適用於任何實際存在的實現。 (語言律師反對它,但沒有成功的編譯器會默默地破壞那么多遺留代碼。)如果你必須進行指針轉換(見下文),則有alignas() 但無論如何,結果將是實現定義的,這就是我們檢查轉換和移動測試值的結果的原因。

無論如何,並不是說您可能在現代CPU上使用它,這是一個經過考驗的C ++ 14版本,可以檢查那些不可移植的假設:

#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <vector>

using std::cout;
using std::endl;
using std::size_t;
using std::sqrt;
using std::uint32_t;

template <typename T, typename U>
  inline T reinterpret(const U x)
/* Reinterprets the bits of x as a T.  Cannot be constexpr
 * in C++14 because it reads an inactive union member.
 */
{
  static_assert( sizeof(T)==sizeof(U), "" );
  union tu_pun {
    U u = U();
    T t;
  };

  const tu_pun pun{x};
  return pun.t;
}

constexpr float source = -0.1F;
constexpr uint32_t target = 0x5ee66666UL;

const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U;
const bool is_little_endian = after_rshift == target;

float est_sqrt(const float x)
/* A fast approximation of sqrt(x) that works less well for subnormal numbers.
 */
{
  static_assert( std::numeric_limits<float>::is_iec559, "" );
  assert(is_little_endian); // Could provide alternative big-endian code.

 /* The algorithm relies on the bit representation of normal IEEE floats, so
  * a subnormal number as input might be considered a domain error as well?
  */
  if ( std::isless(x, 0.0F) || !std::isfinite(x) )
    return std::numeric_limits<float>::signaling_NaN();

  constexpr uint32_t magic_number = 0x1fbb4000UL;
  const uint32_t raw_bits = reinterpret<uint32_t,float>(x);
  const uint32_t rejiggered_bits = (raw_bits >> 1U) + magic_number;
  return reinterpret<float,uint32_t>(rejiggered_bits);
}

int main(void)
{  
  static const std::vector<float> test_values{
    4.0F, 0.01F, 0.0F, 5e20F, 5e-20F, 1.262738e-38F };

  for ( const float& x : test_values ) {
    const double gold_standard = sqrt((double)x);
    const double estimate = est_sqrt(x);
    const double error = estimate - gold_standard;

    cout << "The error for (" << estimate << " - " << gold_standard << ") is "
         << error;

    if ( gold_standard != 0.0 && std::isfinite(gold_standard) ) {
      const double error_pct = error/gold_standard * 100.0;
      cout << " (" << error_pct << "%).";
    } else
      cout << '.';

    cout << endl;
  }

  return EXIT_SUCCESS;
}

更新

這是reinterpret<T,U>()的另一種定義,它避免了類型懲罰。 您還可以在現代C中實現type-pun,標准允許它,並將函數稱為extern "C" 我認為類型懲罰比memcpy()更優雅,類型安全並且與該程序的准功能樣式一致。 我也不認為你獲得了太多,因為你仍然可以從假設的陷阱表示中得到未定義的行為。 此外,clang ++ 3.9.1 -O -S能夠靜態分析類型 - 雙關語版本,將變量is_little_endian優化為常數0x1 ,並消除運行時測試,但它只能將此版本優化為單個 -指令存根。

但更重要的是,這些代碼不能保證在每個編譯器上都可以移植。 例如,一些舊計算機甚至無法准確地處理32位內存。 但在這些情況下,它應該無法編譯並告訴你原因。 沒有任何編譯器會突然間無緣無故地破壞大量的遺留代碼。 雖然標准在技術上允許這樣做,並且仍然說它符合C ++ 14,但它只會發生在與我們期望的完全不同的架構上。 如果我們的假設是如此無效以至於某些編譯器會將float和32位無符號整數之間的類型 - 雙關語變為危險的錯誤,我真的懷疑如果我們只使用memcpy()這個代碼背后的邏輯將會支持memcpy()而不是。 我們希望代碼在編譯時失敗,並告訴我們原因。

#include <cassert>
#include <cstdint>
#include <cstring>

using std::memcpy;
using std::uint32_t;

template <typename T, typename U> inline T reinterpret(const U &x)
/* Reinterprets the bits of x as a T.  Cannot be constexpr
 * in C++14 because it modifies a variable.
 */
{
  static_assert( sizeof(T)==sizeof(U), "" );
  T temp;

  memcpy( &temp, &x, sizeof(T) );
  return temp;
}

constexpr float source = -0.1F;
constexpr uint32_t target = 0x5ee66666UL;

const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U;
extern const bool is_little_endian = after_rshift == target;

但是,Stroustrup等人在C ++核心指南中推薦使用reinterpret_cast

#include <cassert>

template <typename T, typename U> inline T reinterpret(const U x)
/* Reinterprets the bits of x as a T.  Cannot be constexpr
 * in C++14 because it uses reinterpret_cast.
 */
{
  static_assert( sizeof(T)==sizeof(U), "" );
  const U temp alignas(T) alignas(U) = x;
  return *reinterpret_cast<const T*>(&temp);
}

我測試的編譯器也可以將其優化為折疊常數。 Stroustrup的推理是[原文如此]:

reinterpret_cast的結果訪問到與聲明類型的對象不同的類型仍然是未定義的行為,但至少我們可以看到一些棘手的事情正在發生。

設y = sqrt(x),

從log(y)= 0.5 * log(x)(1)的對數屬性得出

將普通float解釋為整數給出INT(x)= Ix = L *(log(x)+ B - σ)(2)

其中L = 2 ^ N,N是有效數的位數,B是指數偏差,σ是調整近似值的自由因子。

結合(1)和(2)給出:Iy = 0.5 *(Ix +(L *(B-σ)))

在代碼中寫為(*(int*)&x >> 1) + 0x1fbb4000;

找到σ使常量等於0x1fbb4000並確定它是否是最優的。

添加wiki測試工具來測試所有float

對於許多float ,近似值在4%以內,但對於次正常數值則非常差。 因人而異

Worst:1.401298e-45 211749.20%
Average:0.63%
Worst:1.262738e-38 3.52%
Average:0.02%

請注意,如果參數為+/- 0.0,則結果不為零。

printf("% e % e\n", sqrtf(+0.0), sqrt_apx(0.0));  //  0.000000e+00  7.930346e-20
printf("% e % e\n", sqrtf(-0.0), sqrt_apx(-0.0)); // -0.000000e+00 -2.698557e+19

測試代碼

#include <float.h>
#include <limits.h>
#include <math.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

float sqrt_apx(float f) {
  const int result = 0x1fbb4000 + (*(int*) &f >> 1);
  return *(float*) &result;
}

double error_value = 0.0;
double error_worst = 0.0;
double error_sum = 0.0;
unsigned long error_count = 0;

void sqrt_test(float f) {
  if (f == 0) return;
  volatile float y0 = sqrtf(f);
  volatile float y1 = sqrt_apx(f);
  double error = (1.0 * y1 - y0) / y0;
  error = fabs(error);
  if (error > error_worst) {
    error_worst = error;
    error_value = f;
  }
  error_sum += error;
  error_count++;
}

void sqrt_tests(float f0, float f1) {
  error_value = error_worst = error_sum = 0.0;
  error_count = 0;
  for (;;) {
    sqrt_test(f0);
    if (f0 == f1) break;
    f0 = nextafterf(f0, f1);
  }
  printf("Worst:%e %.2f%%\n", error_value, error_worst*100.0);
  printf("Average:%.2f%%\n", error_sum / error_count);
  fflush(stdout);
}

int main() {
  sqrt_tests(FLT_TRUE_MIN, FLT_MIN);
  sqrt_tests(FLT_MIN, FLT_MAX);
  return 0;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM