眇眇泛泛

枝葉末節な技術ネタとか

MSVCのstd::powの挙動がちょっと気持ち悪くなっている話

2025/10/18日での、MSVC STL cmathのpowの話
2乗(整数指数時)のpow関数はx*xを返すようになった(戻した)らしい。 詳しくはここを参考。 簡単にはstd::powが呼び出していたUCRTのpowのせいらしい。

It turns out that the UCRT mishandles squaring in a small fraction of cases, returning answers that are off by 1 ULP (unit in the last place).

下記はMSVC STLの実装。

// https://github.com/microsoft/STL/blob/57c30cabbaa6bb77a59a2acee21e2c6ed7b22322/stl/inc/cmath

template <class _Ty1, class _Ty2, _STD enable_if_t<_STD is_arithmetic_v<_Ty1> && _STD is_arithmetic_v<_Ty2>, int> = 0>
_NODISCARD _STD _Common_float_type_t<_Ty1, _Ty2> pow(_Ty1 _Left, _Ty2 _Right) noexcept /* strengthened */ {
    if constexpr (_STD _Is_nonbool_integral<_Ty2>) {
        if (_Right == 2) {
            return static_cast<double>(_Left) * static_cast<double>(_Left); // TRANSITION, see GH-5768
        }
    }

    return _CSTD pow(static_cast<double>(_Left), static_cast<double>(_Right));
}

整数指数のときのみの分岐なので、pow(x, 2.0) などは依然としてUCRTの精度問題の影響を受ける模様。 実際、pow(x, 2) とpow(x, 2.0) を比較する簡単なテストをしてみると、

#include <print>
#include <cmath>
#include <random>

int main() {
  constexpr int n = 10000000;
  int mismatch = 0;
  
  std::mt19937 gen(std::random_device{}());
  std::uniform_real_distribution<double> dist(-1e2, 1e2);
  
  for (int i = 0; i < n; ++i) {
    double x = dist(gen);
    double res1 = std::pow(x, 2);
    double res2 = std::pow(x, 2.0);
  
    if (res1 != res2) {
      ++mismatch;
      if (mismatch < 10) {
        std::print("Mismatch example ({}): x = {:>20.17}, pow(x, 2) = {:>20.17}, pow(x, 2.0) = {:>20.17}\n",
          mismatch, x, res1, res2
        );
      }
    }
  }
  
  std::print("Total mismatches: {} out of {}\n", mismatch, n);

  return 0;
}
Mismatch example (1): x =  37.055681642888231, pow(x, 2) =   1373.123542019084, pow(x, 2.0) =  1373.1235420190837
Mismatch example (2): x =  88.463865152448363, pow(x, 2) =  7825.8554377105684, pow(x, 2.0) =  7825.8554377105675
Mismatch example (3): x =  30.671320123173473, pow(x, 2) =  940.72987809818596, pow(x, 2.0) =  940.72987809818608
Mismatch example (4): x = -77.194875334082354, pow(x, 2) =  5959.0487778445158, pow(x, 2.0) =  5959.0487778445167
Mismatch example (5): x = -22.270302890772342, pow(x, 2) =  495.96639084674297, pow(x, 2.0) =  495.96639084674291
Mismatch example (6): x =  68.469678865561832, pow(x, 2) =  4688.0969239531642, pow(x, 2.0) =  4688.0969239531651
Mismatch example (7): x =  88.304096770406147, pow(x, 2) =  7797.6135064372538, pow(x, 2.0) =  7797.6135064372529
Mismatch example (8): x = 0.83606504944175697, pow(x, 2) = 0.69900476689804758, pow(x, 2.0) = 0.69900476689804747
Mismatch example (9): x = -20.204235227699698, pow(x, 2) =  408.21112113622144, pow(x, 2.0) =  408.21112113622149
Total mismatches: 4174 out of 10000000

そこそこの確率で一致しない。
まぁ、2乗でpow関数なんてそもそも使わないし、あんまり気にならないといえば気にならない、でもちょっと気持ち悪いねって感じ。
そもそもVS2019 16.8以降、powの精度がおかしくなったから修正したという話なので、気持ち悪い状態からちょっと気持ち悪いかも、くらいになったと考えればよいか。


至極どうでもいいことだが、2乗の時に"pow関数"みたいなものをなぜ使わないのか、の説明がしづらい。
「stdのpowは一般的なnに対しては有効だけど、2乗や3乗のようなケースではオーバヘッドが大きいよ(知識マウント)」
「え、2乗のケースはstd::pow(x, 2) って書いてもコンパイラがx*xと同じコード吐き出すでしょ?MSVCの実装はそうやってるし」
みたいな反論の隙を与えてしまう。

MSVCのstd::powを使うときは、指数は整数型よりdoubleにした方が一分岐省略できる、というトリビアで、今後は知識マウントを取りたい所存。


本文とは全然関係ないが、VS 2026の配色テーマ名がやや面白い。 これからは「好きな配色:月の光の輝き」とか自己紹介できるのアツい。 どうせだったらマンゴーパラダイスとかも「芳醇な果実たちの楽園」みたいにしてほしかった。