古(いにしえ)の技術、固定少数点数とは?

少数を100倍とか10000倍とかして整数で扱う手法です。

浮動小数 3.14 ⇒ 固定少数 314

今のゲーム開発では使わなくなったんじゃないでしょうか。
正確には「固定少数点数」ですが、長いので「固定少数」と呼ぶことにします。

メリット
1.昔のハードウェアでは float や double より int の方が計算が速かった。
2.float や double 特有の誤差が出ない。

デメリット
1.int を float や double に変換するのが面倒。
2.速度の問題なら、今のハードウェアでは float か double で良い。
3.誤差の問題なら、誤差が出ない実数型を使えば良い。

固定少数は必要か?

限られた状況では、選択肢のひとつになります。

例)
1.float や double の計算が遅いハードウェア向けの環境で開発している。
2.そもそも、float や double が使えない環境。
3.ハードウェア性能は十分だが、誤差が出ない実数型の計算が遅いため、処理速度が重要な状況で使えない。

こういった状況は今ではかなり特殊なので、これから(もう既に?)ロストテクノロジーになると思っています。

誤差が出ない実数型
C++

boost::multiprecision の多倍長浮動小数点数を使う。

C#

decimal 型を使う。

何故「固定」なのか?

float や double は「浮動」小数点数と呼びます。
値によって小数点の位置が変わる(小数点以下第何位までを扱うかが変わる)ためです。

これに対して固定少数点数は、小数点の位置をあらかじめ決めておき、位置は変えません。
変えても良いですが、それはもう「固定」ではなく、float や double と実装方法が違う浮動小数点数になります。

自作する

C++

#include <cstddef>
#include <cstdint>
#include <iostream>

class Fraction {
  // 小数点以下第何位までを扱うか? 100 なら2位まで。「ゲタを履かせる」という。
  static const std::size_t DENOMINATOR{ 10000 };

  // ゲタを履かせた値
  std::int_fast64_t value{ 0 };
public:
  explicit Fraction(const float source_value) {
	value = static_cast<std::int_fast64_t>(source_value * DENOMINATOR);
  }

  /* デフォルトコンストラクタなどは割愛 */

  // float に変換して返す。
  float ToFloat() const {
    return static_cast<float>(value) / static_cast<float>(DENOMINATOR);
  }
  explicit operator float() const { return ToFloat(); }

  // += 演算子のオーバーライド
  Fraction& operator+=(const Fraction& x) {
    value += x.value;
    return *this;
  }

  /* 以下略… */
};

using namespace std;

int main() {
  Fraction f1(3.14f);
  Fraction f2(0.01f);
  f1 += f2;
  cout << static_cast<float>(f1) << endl;
}

// 実行結果 3.15

使い勝手を良くするには、全てのオペレーターをオーバーライドする必要が出て来るので、かなり面倒です。
コードを書くよりも、テストに時間がかかります。

boost::multiprecision や decimal を使うよりも処理が速くなるなら、作る意味はあるかも知れません。
必要になる状況はかなり限定的で、期待できる効果も微々たるものです。
高速化が必要なら、もっと影響が大きい部分を優先して高速化するべきで、他に選択肢がないときに「仕方なく自作する」程度のものです。

固定少数クラスを自作するときに気を付ける点
float や double にキャストするときに誤差が出る


固定少数のままなら誤差は出ませんが、float や double にキャストするときに誤差が出ます。
必ず出るわけではないですが、float や double は、誤差が出る前提で扱うべきものです。

文字列に変換するのが目的なら、float や double への変換を行わず、固定少数のまま文字列に変換するべきです。
↑のサンプルコードの Fraction クラスに ToChar() とか、ToString() を実装すると良いです。

ビットシフトで計算する方が速い(かも知れない)

↑のサンプルコードでは、100や10000など、10の指数を使って「ゲタを履かせて」います。
人間にとって計算しやすいからです。

float が遅かったころは、10の指数の乗算・除算も遅く、コンパイラの最適化もまだまだ微妙だったので、ビットシフト(シフト演算)を使っていました。
ビットシフトなので、2の指数(2 4 8 16 32…)を使って「ゲタを履かせる」ことになります。
コンピューターにとって計算は速いですが、人間にとっては慣れないと難しいです(2や4など1桁ならともかく…)。
なので、慣れないとテストやデバッグに時間がかかります。
今は余程のことがなければ、乗算・除算をビットシフトで代用することはないと思います(コンパイラがいい感じに最適化してくれるはず)。

ゲームには float と double を使ってはいけないパラメータがある

float と double は誤差が出るので、誤差を許さない数値(ガチャ確率や UI に表示する装備のパラメータなど)で使うのは間違った実装です。
間違った実装をしているシステムをいろんなところで見ています。
何故そうなるのかは謎です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です