float32の中にNaNとかがいくつあるか数える

Posted on

お久ぶりでございます。最近はUnityでfloatと格闘しております(趣味で)。

ふと気になったのが、「ところで、floatの32ビットの中にNaNって何個ぐらいあるんだろう?」。

そんなわけで数えてみました。

ソースコード

#include <iostream>
#include <cmath>

int main() {
  uint64_t normal = 0;
  uint64_t subNormal = 0;
  uint64_t positiveZero = 0;
  uint64_t negativeZero = 0;
  uint64_t neutralZero = 0;
  uint64_t positiveInfinity = 0;
  uint64_t negativeInfinity = 0;
  uint64_t notANumber = 0;
  uint64_t unknown = 0;
  uint64_t total = 0;
  std::cerr << "Start" << std::endl;
  for (int64_t i = 0; i <= 0xffffffff; ++i) {
    total++;
    uint32_t const j = static_cast<uint32_t>(i);
    float const f = *reinterpret_cast<float const*>(&j);
    switch (std::fpclassify(f)) {
      case FP_INFINITE:
        if (f > 0) {
          positiveInfinity++;
        } else if(f < 0) {
          negativeInfinity++;
        } else {
          throw std::logic_error("Zero infinity?");
        }
        break;
      case FP_NAN:
        notANumber++;
        break;
      case FP_NORMAL:
        normal++;
        break;
      case FP_SUBNORMAL:
        subNormal++;
        break;
      case FP_ZERO:
        if (1/f > 0) {
          positiveZero++;
        } else if(1/f < 0) {
          negativeZero++;
        } else {
          neutralZero++;
        }
        break;
      default:
        unknown++;
        std::cerr << "Unknown: " << f << std::endl;
        break;
    }
  }
  std::cout << "Total: " << total << "(" << (((double)total)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Normal: " << normal << "(" << (((double)normal)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "SubNormal: " << subNormal << "(" << (((double)subNormal)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Positive zero: " << positiveZero << "(" << (((double)positiveZero)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Negative zero: " << negativeZero << "(" << (((double)negativeZero)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Neutral zero: " << neutralZero << "(" << (((double)neutralZero)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Positive infinity: " << positiveInfinity << "(" << (((double)positiveInfinity)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Negative infinity: " << negativeInfinity << "(" << (((double)negativeInfinity)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Not A Number: " << notANumber << "(" << (((double)notANumber)/((double)total) * 100.0) << "%)" << std::endl;
  std::cout << "Unknown: " << unknown << "(" << (((double)unknown)/((double)total) * 100.0) << "%)" << std::endl;

  return 0;
}

かなり素直なコードです。40億回ループを回しても30秒もかかりません。最近のプロセッサすごい(5年前のモデルだけど)!

少しテクニックがあるとすれば…

  • std::fpclassify を使うと種類が返ってきます。知らんかった。
  • ゼロが正か負か判断するために(f == -0.0)を最初書いたのですが、これは必ずfalseらしい(CLion談)ので、1で割ることでプラスマイナスどちらかの無限大に飛んでいくことを利用して判断してます。
  • forでカウンターで回す限りはuint64_tを使わないとダメです。最初uint32_tにしたら0xfffffffの次が0になって無限ループになってしまいました。do while文ならuint32_tでも行けるかもしれない。
  • カテゴリ多すぎだろ気が狂うわ

結果

Total: 4294967296(100%)
Normal: 4261412864(99.2188%)
Sub Normal: 16777214(0.390625%)
Positive zero: 1(2.32831e-08%)
Negative zero: 1(2.32831e-08%)
Neutral zero: 0(0%)
Positive infinity: 1(2.32831e-08%)
Negative infinity: 1(2.32831e-08%)
Not A Number: 16777214(0.390625%)
Unknown: 0(0%)

使ってる環境は次の通り:

  • Ryzen Threadripper 1950x
  • Windows 10 64bit、よくしらんけどこの記事の時点で最新
  • コンパイラはVS2022、よくしらんけどこの記事の時点で最新

所感

ゼロと無限大はちょうど1つずつあるんだなぁというところに素直に関心しました。

非正規化数とnot a numberの数が等しいのも面白いです。なんでなのかは知らん。IEEE754の仕様書読んで教えてくれ。

IEE754はまぁまぁ複雑ですが、それでも変なケースを1%以下に抑えてるのはえらい…と言って良いのかな?

「ぼくのかんがえたさいきょうの浮動小数点フォーマット」を考えてみたくなりました。

まとめ

小学生の夏休みの自由研究でも許されなさそう