2025/04/06(日)非正規化数の計算は遅い?

はじめに

よく知られているように、IEEE754に従う浮動小数点数はフォーマットの一部に非正規化数(denormalized number, denormal number, subnormal number)という領域があります。倍精度では、絶対値が2-1074から2-1022までの0に近い領域で、非常に小さな数が表現できるのと引き換えに、この範囲では仮数部の長さが通常の53bitより短く精度が低下します。

ところで、我々の業界では、計算の途中に非正規化数が出てくると計算速度が極端に低下(50~100倍ほど)すると言われていて、いくつかの精度保証アルゴリズムは非正規化数がなるべく出現しないように設計されています。しかし、実際にそんなに遅くなるのかどうか、実測したことは無かったので、計測してみました。

使用プログラム

使ったのは次のようなプログラムです。加算、乗算、除算 (減算は加算と同じだろうから省略) について、引数に非正規化数が含まれている場合と含まれていない場合の計算速度の比を計測しています。10億回繰り返しています。volatileに代入したり、計算結果を表示したりして、最適化で計算本体が消されないように小細工しています。実際にやっている計算は
  • 正規化数 + 非正規化数 = 正規化数
  • 非正規化数 * 正規化数 = 非正規化数
  • 非正規化数 / 正規化数 = 非正規化数
です。もしかしたら計算結果が正規化数か否かでも変わるかもしれませんが、そこまでは調べていません。
#include <iostream>
#include <cmath>
#include <chrono>

double vc(double x)
{
	volatile double tmp = x;
	return tmp;
}

int main()
{
	std::chrono::system_clock::time_point ts0, ts1, ts2, ts3;
	int i, j;
        double a, b;
        double t0, t1, r0, r1, r2;
	

	a = vc(1);
	b = vc(std::pow(2., -1021));

	std::cout << "add normal\n";
        ts0 = std::chrono::system_clock::now();
	for (i=0; i<10000; i++) {
		for (j=0; j<100000; j++) {
			a += b;
		}
	}
        ts1 = std::chrono::system_clock::now();
        t0 = std::chrono::duration_cast<std::chrono::nanoseconds>(ts1-ts0).count()/1e9;
        std::cout << t0 << "\n";

	std::cout << a << "\r              \n"; // dummy

	a = vc(1);
	b = vc(std::pow(2., -1023));

	std::cout << "add denormal\n";
        ts2 = std::chrono::system_clock::now();
	for (i=0; i<10000; i++) {
		for (j=0; j<100000; j++) {
			a += b;
		}
	}
        ts3 = std::chrono::system_clock::now();

        t1 = std::chrono::duration_cast<std::chrono::nanoseconds>(ts3-ts2).count()/1e9;
        std::cout << t1 << "\n";

	r0 = t1 / t0;
	std::cout << "\nadd ratio\n";
        std::cout << "denormal / normal = " << r0 << "\n";

	std::cout << a << "\r              \n"; // dummy

	a = vc(std::pow(2., -1021));
	b = vc(1);

	std::cout << "mul normal\n";
        ts0 = std::chrono::system_clock::now();
	for (i=0; i<10000; i++) {
		for (j=0; j<100000; j++) {
			a *= b;
		}
	}
        ts1 = std::chrono::system_clock::now();
        t0 = std::chrono::duration_cast<std::chrono::nanoseconds>(ts1-ts0).count()/1e9;
        std::cout << t0 << "\n";

	std::cout << a << "\r              \n"; // dummy

	a = vc(std::pow(2., -1023));
	b = vc(1);

	std::cout << "mul denormal\n";
        ts2 = std::chrono::system_clock::now();
	for (i=0; i<10000; i++) {
		for (j=0; j<100000; j++) {
			a *= b;
		}
	}
        ts3 = std::chrono::system_clock::now();

        t1 = std::chrono::duration_cast<std::chrono::nanoseconds>(ts3-ts2).count()/1e9;
        std::cout << t1 << "\n";

	r1 = t1 / t0;
	std::cout << "\nmul ratio\n";
        std::cout << "denormal / normal = " << r1 << "\n";

	std::cout << a << "\r              \n"; // dummy

	a = vc(std::pow(2., -1021));
	b = vc(1);

	std::cout << "div normal\n";
        ts0 = std::chrono::system_clock::now();
	for (i=0; i<10000; i++) {
		for (j=0; j<100000; j++) {
			a /= b;
		}
	}
        ts1 = std::chrono::system_clock::now();
        t0 = std::chrono::duration_cast<std::chrono::nanoseconds>(ts1-ts0).count()/1e9;
        std::cout << t0 << "\n";

	std::cout << a << "\r              \n"; // dummy

	a = vc(std::pow(2., -1023));
	b = vc(1);

	std::cout << "div denormal\n";
        ts2 = std::chrono::system_clock::now();
	for (i=0; i<10000; i++) {
		for (j=0; j<100000; j++) {
			a /= b;
		}
	}
        ts3 = std::chrono::system_clock::now();

        t1 = std::chrono::duration_cast<std::chrono::nanoseconds>(ts3-ts2).count()/1e9;
        std::cout << t1 << "\n";

	r2 = t1 / t0;
	std::cout << "\ndiv ratio\n";
        std::cout << "denormal / normal = " << r2 << "\n";

	std::cout << a << "\r              \n"; // dummy

	std::cout << "ratio (add/mul/div): " << r0 << " " << r1 << " " << r2 << "\n";
}

実験結果

これをg++で-O3をつけてコンパイル、実行して、正規化数の場合の計算時間を1としたときの非正規化数の計算時間を、手元に転がっていたPCで片っ端から調べてみました。次の表にまとめます。
CPU加算乗算除算
Intel Core M-5Y10c (Broadwell)0.99933842.199514.3503
Intel Core i5-6500T (Skylake)0.92403433.542811.956
Intel Core i9-7900X (Skylake)0.96088633.876411.9609
Intel Pentium CPU 4415Y (Kaby Lake)0.99709934.407912.1162
Intel Core i5-8250U (Kaby Lake R)0.99220934.503712.1195
11th Gen Intel Core i7-1195G70.9998429.120210.577
Intel Celeron N4120 (Gemini Lake Refresh)57.132643.77513.7602
Intel N100 (Alder Lake-N)95.714171.82423.5843
AMD Ryzen 7 7700 (Zen4)0.9816351.553461.39528
AMD Ryzen 7 8840U (Zen4)0.990941.329991.40047
Apple M10.9648730.9992541.00117
これを見ると、IntelのCPUは加算は遅くならないが乗除算が遅い、ATOM系は加算も遅い、AMDやApple Siliconはほとんど遅くならない、ことが読み取れます。

おわりに

個人で所有しているCPUの種類には限界があってなかなか網羅的な調査は難しいですが、いろいろ検索して情報を集めてみた結果、どうやら次の表のような感じっぽいです。(o=ペナルティなし、x=ペナルティあり)
CPU加算乗算除算
Intel SandyBridgeより古いxxx
AMD Bulldozerxxx
Intel SandyBridge以降11世代までoxx
Intel ATOM系xxx
Intel KNLoox
AMD Zenooo
Apple Siliconooo
SandyBridgeより古いのとかBulldozerとかKNLとか、実機で試すのは大変ですが、いつかやってみたいものです。あるいはどなたか動かしてくれないかしら。

Intel 12世代以降はどうなってるの?

ところで、実験を見て気になるのはN100です。加算も遅いという散々な結果ですが、N100って、Intel 12世代以降のいわゆるEコアで出来ているはず。ということは、12世代以降のCPUで運悪くEコアに当たってしまうと加算ですら非正規化数のペナルティが発生する? 自分はIntelの12世代以降は所有していないので試せないのですが、気になります。

追記 (2025年4月7日)

未開封のCore Ultra 7 155UのPCがあったのに気づいたのでセットアップ試してみました。こいつはCore Ultra シリーズ1というやつで、Pコア、Eコア、LP(低電力)Eコアがそれぞれ2,8,2コアあり、PコアはHyper Threadingで倍になるので全部で14threadというなかなかややこしい構成になっています。/proc/cpuinfoを読み出してじっと睨んでると14の論理コアごとに微妙に違っていて、
processorcore idcache size
0812288 KB
1812288 KB
21212288 KB
31212288 KB
4012288 KB
5112288 KB
6212288 KB
7312288 KB
8412288 KB
9512288 KB
10612288 KB
11712288 KB
12322048 KB
13332048 KB
0-3がPコア、4-11がEコア、12-13がLP Eコアと推定できます。

これを使って、
$ taskset 0x00000001 ./a.out
$ taskset 0x00000010 ./a.out
$ taskset 0x00001000 ./a.out
のようにbitmaskで使用していいprocessorを指定して実行しました。結果は、
CPU加算乗算除算
Core Ultra 7 155U (P core)0.96551641.671411.1905
Core Ultra 7 155U (E core)94.278271.638723.4713
Core Ultra 7 155U (LP E core)95.088671.63223.4997
の通りでした。Eコアは全演算にペナルティがあって、Pコアは乗除算にペナルティがあります。

なお、るふぁ先生が調査して下さった結果を合わせると、Intelの12世代以降は、
CPU加算乗算除算
Intel 12-14世代 (Pコア)oxx
Intel 12-14世代 (Eコア)xxx
Intel Core Ultra 第1シリーズ (Pコア)oxx
Intel Core Ultra 第1シリーズ (Eコア)xxx
Intel Core Ultra 第2シリーズ (Pコア)oxx
Intel Core Ultra 第2シリーズ (Eコア)oxx
と、Core Ultra 第2シリーズでEコアが良くなったっぽいです。

2025/03/18(火)数学ソフトウェアとフリードキュメント

昨日は、数学ソフトウェアとフリードキュメント 37という集まりで、kvライブラリについて50分間、話をしてきました。数値計算の誤差がどのくらい怖いか、という「脅し」のパートが受けてたみたいです。kvライブラリのホームページに置いてあるスライドにちょっと追加しただけではありますが、スライドが欲しいという話があったのでここに置いておきます。

2025/03/15(土)kv-0.4.58

kvライブラリを0.4.58にアップデートしました。

今回も大した変更はありません。
  • ODE Solverで、精度保証に失敗した場合ごく稀にstep sizeを小さくしてのretryをせずにエラーで止まってしまうことがあったのを修正。
  • PSAのpow(x, y)で、yが整数でない定数だがpsa型でない場合の動作を改善。
  • vleq.hppの精度を改善 (高安先生の要請による)
くらいです。

数学ソフトウェアとフリードキュメント 37でkvについてしゃべるので、いい機会なのでここまで溜まっていたアップデートを吐き出したかったという理由もありました。

2024/08/27(火)kv-0.4.57

kvライブラリを0.4.57にアップデートしました。

今回は、
  • 以前にるふぁさんにご指摘頂いた、defint_autostepで使用する級数の次数を1にしたときに正しく計算できていなかったバグの修正。
  • ode_maffineに自動微分型を指定し、なおかつcallback関数を指定したとき、 callback関数が初期値に関する微分の情報をも渡してくれるように変更。
の2点です。後者はとある研究で必要になって機能追加しました。

大したアップデートではないですが、一応2つ溜まったので。

2024/05/07(火)Ubuntu 24.04 インストール (オマケ) WSL2

はじめに

Ubuntu 24.04 インストールシリーズのオマケとして、Windows 10/11のWSL2のインストールについて書いてみます。

WSLについて書くのは、2016年のbash on Ubuntu on Windowsを試してみるの記事以来です。当時はまだWSLとは呼ばれていませんでした。WSL1からWSL2になって仮想マシンを使うようになって性能及び互換性が向上し、またWSL-gとなってX serverを別にインストールしなくてもGUIアプリを動かすことができるようになりました。ちょうどWSL2の中に入れられるLinuxとしてUbuntu 24.04も出たので、ここらでインストール方法についてまとめてみました。Windows 10でもWindows 11でも基本的には同じと思いますが、まっさらなWindows 10に入れてみました。

マシンに慣れていない人でも分かるように、画像をたくさん貼り付けてみました。プログラミングの演習のためにパソコンを設定する大学1年生あたりを想定しています。今更ですが買ったばかりのマシンに設定することを想定するなら、Windows 11でやれば良かった…

インストール

VT-x

WSL2になって仮想マシンを使うようになって性能が向上したのですが、それと引き換えに、VT-xと呼ばれる機能をBIOS(正確にはUEFI?)で有効にする必要があります。ほとんどのマシンで有効になっているとは思いますが、もしなっていない場合、BIOSに入って有効にする必要があります。入り方はマシン毎に千差万別なので説明のしようもありませんが、起動時にDEL連打、もしくはF2連打のマシンが多い気がします。うまくBIOSに入れたら、Intel VT-x (Intel CPUの場合) もしくはAMD-V Virtualization (AMD CPUの場合)を探してオンにして下さい。

Windowsの機能の有効化

「Windowsの機能の有効化または無効化」(Windows10なら、スタートボタン→Windowsシステムツール→コントロールパネル→プログラム→Windowsの機能の有効化または無効化)で、
  • Linux用Windowsサブシステム
  • 仮想マシンプラットフォーム
の2つにチェックを入れて再起動する必要があります。

wsl01.png

WSLを最新にアップデート

自分が試した限り、ストアでインストールする前にこの作業をやっておかないとWSL1でインストールされてしまい、後でアップデートの作業が必要になりました。先にこれをやっておくのがお薦めです。

まず、Windows PowerShellを管理者モードで起動します。Windows10では、スタートボタンを右クリックし、「Windows PowerShell(管理者)」を選択します。

wsl02.png
wsl03.png


ここで、「wsl --version」としてみると、自分の場合は--versionというオプションは無いと言われました。これは、wslコマンドがとても古いことを意味しています。「wsl --update」と打ち込んで最新にします。

wsl04.png


これにより、
WSL バージョン: 2.1.5.0
カーネル バージョン: 5.15.146.1-2
WSLg バージョン: 1.0.60
MSRDC バージョン: 1.2.5105
Direct3D バージョン: 1.611.1-81528511
DXCore バージョン: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp
Windows バージョン: 10.0.19045.4291
になりました。

Microsoft StoreからUbuntu 24.04をインストール

ここでMicrosoft Storeを起動し、Ubuntu 24.04を検索してインストールします。どういうわけだか、「WSL」では検索に引っかからず、「ubuntu」だと見つかりました。

wsl05.png
wsl06.png


Ubuntu 24.04 LTSを「入手」でインストールします。終わったら、そのまま「開く」で起動して下さい。スタートメニューに登録された「Ubuntu 24.04 LTS」を起動でもOKです。

wsl07.png


WSLのターミナルが起動し、しばらく待った後、「Enter new UNIX username:」と聞かれるので、ユーザ名(たぶん半角英数のみ、先頭はアルファベット)を決めます。次にパスワードを聞かれるので、2度入力して下さい。このパスワードはアプリケーションのインストール等、管理者(root)になる必要があるときに何度も聞かれるので、忘れないようにしてください。

wsl08.png


この状態で、Windows PowerShellの方で「wsl --list -v」してみると、ちゃんとUbuntu 24.04がVERSION 2で入ったことが確認できました。

wsl09.png

ファイルのやりとり

この状態で、Windows側からWSL側のファイルを見るには、エクスプローラに追加されている「Linux」という項目を使うことができます。

wsl12.png


たとえばユーザ名が「kashi」なら、ここからhome→kashiと辿っていけばホームディレクトリの内容が見えます。また、「\\wsl.localhost\」とエクスプローラに打ち込んでも同じです。

wsl13.png


逆にWSL側からWindows側のファイルは、「/mnt/c/」からアクセスすることができます。

アプリケーションの追加

ubuntuの最新化

ここから必要なアプリケーションをaptコマンドを使って追加していきますが、その前に絶対必要な作業があります。WSLのターミナルで、
sudo apt update
sudo apt upgrade
として、Ubuntu 24.04を最新にしてください。特に最初の1行が重要で、これをやらないとこの先のaptコマンドでのインストールが失敗してしまいます。

wsl10.png
wsl11.png

日本語化

メッセージ等を日本語化するため、WSLのターミナルで
sudo apt install -y language-pack-en
sudo apt install -y language-pack-ja
sudo update-locale LANG=ja_JP.UTF8
として、いったんWSLターミナルを閉じて再度スタートメニューから起動します。language-pack-enの方はすぐには不要かもですが、無いと何かと不便なのでこちらもついてに入れておきます。自分の環境では、Visual Studio CodeでのWSLターミナルで問題が発生しました。

アプリケーションのインストール

ここまで来れば、後は好きなように環境を整えていきます。以前と違ってWSLターミナルの中ではWindowsのIMEを使って日本語を書くことが出来るようになっています。GUIアプリもgnuplotなどの簡単なアプリなら動きます。自分は、Ubuntu 24.04 インストール (リンク集)のうちからapache, sambaなどを除き、適当に抜粋して次のようなものを入れてみました。
sudo apt install -y texlive-full
sudo apt install -y nkf
sudo apt install -y gnuplot
sudo apt install -y tgif
sudo apt install -y pdfarranger
sudo apt install -y pdftk-java
sudo apt install -y build-essential
sudo apt install -y clang
sudo apt install -y libboost-all-dev
sudo apt install -y default-jdk
sudo apt install -y lua5.4
sudo apt install -y liblua5.4-dev
sudo apt install -y luajit
sudo apt install -y gfortran
sudo apt install -y python3
sudo apt install -y python3-dev
sudo apt install -y python3-numpy
sudo apt install -y python3-scipy
sudo apt install -y python3-matplotlib
sudo apt install -y python3-sympy
sudo apt install -y python3-mpmath
sudo apt install -y ipython3
sudo apt install -y python-is-python3
sudo apt install -y octave
sudo apt install -y octave-dev
sudo apt install -y libgmp-dev
sudo apt install -y libmpfr-dev
sudo apt install -y gcc-multilib
sudo apt install -y g++-multilib
sudo apt install -y nim
sudo apt install -y lv
この段階でも、
  • メモ帳等、任意のwindowsのエディタ
  • (抵抗が無ければ) vi, nano等のLinuxのターミナルで動くエディタ
を用いてhome directoryにファイルを作成し、プログラミングを行ったり、TeXのコンパイルを行う(pdfを見るのはEdge等windowsアプリで)ことが出来ます。

wsl14.png

Visual Studio Codeを使う

Visual Studio Codeという、Windows上で動くエディタがあります。これは、そのWindowsにインストールされたWSLと連携する機能を持っており、なかなか便利です。これをインストールして連携させてみます。

Microsoft Storeで、「visual studio code」で検索すると見つかります。

wsl16.png
wsl17.png


インストールして起動したら、こんな感じになりました。

wsl18.png


まずは、左下の歯車アイコン→Extensionで、「Japanese Language Pack for Visual Studio Code」を検索し、インストールします。「Change Language and Restart」というボタンを押せば、メニュー等が日本語になります。

wsl19.png


同様に、左下の歯車アイコン→拡張機能で、「WSL」を検索し、インストールします。これでWSL連携機能が使えるようになります。いったん、Visual Studio Codeを閉じて、WSLのターミナルで、「code .」(末尾のドットに注意)と入力すると、WSL側のカレントディレクトリが見えた状態でVisual Studio Codeが開きます。「このフォルダの作成者を信頼しますか?」などと聞かれたら「はい」。

wsl20.png


更に上部のメニューで「ターミナル→新しいターミナル」とすれば、WSLのターミナルがVisual Studio Codeの画面内に現れて、以降はVisual Studio Codeの画面内だけで作業が完結するようになります。

wsl21.png


TeXの執筆作業は、こんな感じになりました。この画面では、vscode-pdfというpdfをVisual Studio Codeの中で見ることができるようになる拡張機能を追加しています。LaTeX Workshopという拡張機能を入れるとすごく便利らしいですが、それは検証してません。

wsl15.png

GUIアプリケーションを(無理やり)整える

ここから先はあまりお薦めではないかもです。Ubuntuで普段使っているGUIアプリを無理やり動かしてみます。WSLでは、WSLターミナル上ではWindowsのIMEを使って日本語入力が出来ますが、それ以外のGUIアプリケーションで日本語を入力する手段がありません。ここでは、Ubuntuのかな漢字変換システムをインストールして、それを可能にしてみます。

まず、WSLターミナルで、
sudo apt install -y fcitx5-mozc
を実行します。次に、home directoryの「.profile」の末尾に、
while true; do
  dbus-update-activation-environment --systemd DBUS_SESSION_BUS_ADDRESS DISPLAY XAUTHORITY 2> /dev/null && break
done

export GTK_IM_MODULE=fcitx5
export QT_IM_MODULE=fcitx5
export XMODIFIERS=@im=fcitx5
export INPUT_METHOD=fcitx5
export DefaultIMModule=fcitx5
if [ $SHLVL = 1 ] ; then
  (fcitx5 --disable=wayland -d --verbose '*'=0 &)
  xset -r 49
を書き加え、いったんWSLターミナルを閉じて再度起動します。再起動後、WSLターミナルで、「fcitx5-configtool」を起動します。すると、

wsl22.png


のように「Fcitxの設定」が立ち上がると思います。ここで、真ん中の「←→↑↓」ボタンを使って、左ペインを、
  • キーボード - 英語(US)
  • Mozc
から、
  • キーボード - 日本語
  • Mozc
に変更して下さい。

wsl23.png


のようになると思います。「OK」で閉じて下さい。これで、かな漢字変換が使えるようになっています。例えば、
sudo apt install -y gedit
などとして、geditエディタで日本語が書けるのを確認して下さい。

wsl24.png


他にも、
sudo apt install -y x11-apps
sudo apt install -y firefox
sudo apt install -y firefox-locale-ja
sudo apt install -y lxterminal
sudo apt install -y nautilus
sudo apt install -y evince
sudo apt install -y eog
などを入れました。gnome-terminal, gnome-text-editorは少し不安定な感じがしたので、それぞれlxterminal, geditで代用することにしました。

これで、普段ubuntuで行っている作業が(やや不安定ながらも)WSLで行えるようになりました。例えばTeX執筆は次のような感じで行えます。

wsl25.png

アンインストール

さて、これらのソフトは複雑に連携していて、操作ミスで壊してしまうこともあるかと思います。そこで、きれいさっぱりアンインストールする方法を書いておきます。

WSLのアンインストール

まず、Windows PowerShellを管理者モードで起動し、「wsl --list -v」でインストールされている名前を調べ、「wsl --unregister Ubuntu-24.04」のように登録を解除します。

wsl26.png


解除後、スタートメニューの設定(歯車アイコン)→アプリで、Ubuntu 24.04 LTSを探してアンインストールします。

wsl29.png

Visual Studio Codeのアンインストール

まず、普通にスタートメニューの設定(歯車アイコン)→アプリで、Microsoft Visual Studio Codeを探してアンインストールします。

wsl28.png


これだけでは不十分で、
  • c:\Users\(ユーザ名)\.vscode
  • c:\Users\(ユーザ名)\AppData\Roaming\Code
の2つのフォルダを削除してください。

おわりに

お楽しみいただければ幸いです。おかしなところがあればお知らせ下さい。特に無理やりUbuntuのGUIを使うあたりは、いろいろ改善点があると思います。
OK キャンセル 確認 その他