web-dev-qa-db-ja.com

このフロート平方根近似はどのように機能しますか?

floatsのかなり奇妙ではあるが有効な平方根近似を見つけました。本当に分かりません。このコードが機能する理由を誰かが説明できますか?

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

少しテストして、 std::sqrt()の値を約1〜3%出力します 。私はQuake IIIの 高速逆平方根 を知っており、ここでは似たようなものだと思います(ニュートン反復なし)が、それがどのように機能するか

(注:CとC++コードの両方が有効であるため(コメントを参照) cc ++ の両方にタグを付けました)

51
YSC

(*(int*)&f >> 1)は、fのビット単位表現を右シフトします。このalmostは、指数を2で除算します。これは、平方根を取ることにほぼ相当します。1

なぜalmost? IEEE-754では、実際の指数はe-127です。2 これを2で割るには、e/2-64が必要ですが、上記の近似ではしか得られませんe/2-127。したがって、結果の指数に63を加算する必要があります。これは、そのマジック定数(0x1fbb4000)。

マジック定数の残りのビットは、仮数範囲またはそのようなもの全体で最大誤差を最小化するように選択されていると思います。ただし、分析、反復、またはヒューリスティックのいずれで決定されたかは不明です。


このアプローチは多少移植性がないことを指摘する価値があります。 (少なくとも)次のことを前提としています。

  • プラットフォームは、floatに単精度IEEE-754を使用します。
  • float表現のエンディアン。
  • このアプローチがC/C++の strict-aliasing rules に違反しているという事実により、未定義の動作の影響を受けないこと。

したがって、プラットフォーム上で予測可能な動作を提供することが確実でない限り(および実際、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

70

このalmostが機能する理由についてのOliver Charlesworthの説明を参照してください。コメントで提起された問題に取り組んでいます。

何人かの人々がこれの非移植性を指摘しているので、ここでそれをよりポータブルにするか、少なくともコンパイラーに動作しないかどうかを伝えることができるいくつかの方法があります。

最初に、C++では、_std::numeric_limits<float>::is_iec559_などのコンパイル時に_static_assert_をチェックできます。 intが64ビットの場合、trueではないsizeof(int) == sizeof(float)を確認することもできますが、本当にしたいのは_uint32_t_を使用することです。常に正確に32ビット幅であり、シフトとオーバーフローで明確に定義された動作を行い、奇妙なアーキテクチャにそのような整数型がない場合はコンパイルエラーを引き起こします。いずれにしても、タイプが同じサイズであることをstatic_assert()する必要があります。静的アサーションにはランタイムコストはありません。可能であれば、常にこの方法で前提条件を確認する必要があります。

残念ながら、floatのビットを_uint32_t_に変換してシフトするかどうかのテストは、ビッグエンディアン、リトルエンディアン、またはどちらもコンパイル時の定数式として計算できません。ここでは、それに依存するコードの一部にランタイムチェックを入れていますが、初期化に入れて1回実行することもできます。実際には、gccとclangの両方がコンパイル時にこのテストを最適化できます。

安全でないポインターキャストを使用したくはありません。現実には、バスエラーでプログラムがクラッシュする可能性のあるシステムがいくつかあります。オブジェクト表現を変換するための最大限に移植可能な方法は、memcpy()を使用することです。以下の私の例では、unionを使って型を打ちます。これは、実際に存在する実装で動作します。 (言語弁護士はこれに反対しますが、成功したコンパイラーはそれほど多くのレガシーコードを壊すことはありませんsilently。)ポインター変換を行う必要がある場合(以下を参照) 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()を使用します。コンパイル時にそのコードを失敗させ、その理由を教えてください。

_#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 et al。、 C++ Core Guidelines では、代わりに_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_の結果にオブジェクト宣言された型とは異なる型にアクセスすることは未定義の動作ですが、少なくともトリッキーなことが起こっていることがわかります。

13
Davislor

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に等しくなるようにσを見つけ、それが最適かどうかを判断します。

8

すべてのfloatをテストするためのWikiテストハーネスを追加します。

近似値は多くのfloatで4%以内ですが、非正規数では非常に貧弱です。 [〜#〜] ymmv [〜#〜]

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;
}
6
chux