コグノスケ


link 未来から過去へ表示(*)  link 過去から未来へ表示

link もっと前
2023年10月1日 >>> 2023年9月22日
link もっと後

2023年10月1日

FizzBuzzを速くする4(うまくいかないこともある)

目次: ベンチマーク

FizzBuzzの実装は簡単ですが、可能な限り高速に出力しようとするとなかなか面白い遊びになります。今回はあるCPUでうまくいっても、他のCPUでは効果がないケースをご紹介します。

実験用に4つのコードを用意しました。出力がボトルネックになって測定結果が不必要に遅く見えないよう、vmspliceとバッファリングは最初から実装します。

  • 20231001_fizzbuzz_base.c: 自前のitoaのみ(比較元として使う)
  • 20231001_fizzbuzz_30.c: 30個まとめる最適化
  • 20231001_fizzbuzz_offset.c: オフセット0xf6アルゴリズム(仮)に置き換え
  • 20231001_fizzbuzz_fixed.c: 9桁と10桁を狙い撃ちで最適化

30個まとめて処理する最適化で速くなるのはほぼ確実でしょう。3つ目は、前回(2023年9月23日の日記参照)紹介したオフセット0xf6アルゴリズムです。これも速くなるのはほぼ確実でしょう。

4つ目は、前々回(2023年9月21日の日記参照)紹介した9桁と10桁を狙い撃ちで最適化する方法です。自前のitoa()には効果抜群でしたので、オフセット0xf6アルゴリズムとの相乗効果にも期待したいところです。

省電力PCでの効果

まずは省電力PC(CPU: Pentium J4205)で測定します。

Pentium J4205での実行結果
# 20231001_fizzbuzz_base.c

33.3GiB 0:01:06 [ 512MiB/s] [                               <=>                ]

real    1m6.621s
user    1m4.461s
sys     0m5.356s

# 20231001_fizzbuzz_30.c

33.3GiB 0:00:38 [ 877MiB/s] [                                    <=>           ]

real    0m38.860s
user    0m37.459s
sys     0m4.377s

# 20231001_fizzbuzz_offset.c

33.3GiB 0:00:09 [3.45GiB/s] [         <=>                                      ]

real    0m9.671s
user    0m8.047s
sys     0m3.726s

# 20231001_fizzbuzz_fixed.c

33.3GiB 0:00:08 [3.74GiB/s] [        <=>                                       ]

real    0m8.906s
user    0m6.955s
sys     0m4.216s

いずれの最適化も効いていて、4つ目が最速です。良いですね。

デスクトップPCでの効果

次はデスクトップPC(CPU: Ryzen 7 5700X)で測定します。

Ryzen 7 5700Xでの実行結果
# 20231001_fizzbuzz_base.c

33.3GiB 0:00:15 [2.11GiB/s] [               <=>                                ]

real    0m15.759s
user    0m15.425s
sys     0m1.345s

# 20231001_fizzbuzz_30.c

33.3GiB 0:00:09 [3.64GiB/s] [         <=>                                      ]

real    0m9.152s
user    0m8.886s
sys     0m1.176s

# 20231001_fizzbuzz_offset.c

33.3GiB 0:00:02 [16.2GiB/s] [  <=>                                             ]

real    0m2.063s
user    0m1.762s
sys     0m1.070s

# 20231001_fizzbuzz_fixed.c

33.3GiB 0:00:02 [15.8GiB/s] [  <=>                                             ]

real    0m2.112s
user    0m1.802s
sys     0m1.080s

なんと9桁と10桁狙い撃ちで最適化すると逆に遅くなりました。時間と高速化の度合いをまとめると、

FizzBuzzの種類Pentium J4205の実行時間倍率Ryzen 7の実行時間倍率
自前itoa 1m6.621s- 15.759s-
30個まとめる 38.860s x1.7 9.152s x1.7
オフセット0xf6 9.671s x6.8 2.063s x7.6
9桁10桁狙い撃ち8.906s x7.4 2.112s x7.4

Ryzen 7 5700Xでなぜ遅くなるのか?は内部構造を知らないので何とも言えませんが、あるCPUに効く最適化が他のCPUだと効果がなかったり逆効果になったりすることは往々にしてあるという好例に思います。

ソースコード

ソースコードはこちらからどうぞ。

編集者:すずき(2024/01/23 10:52)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年9月23日

FizzBuzzを速くする3(CPUを変えてみよう)

目次: ベンチマーク

FizzBuzzの実装は簡単ですが、可能な限り高速に出力しようとするとなかなか面白い遊びになります。前回は高速なアルゴリズムを紹介しましたが、CPUを変えたら傾向がどうなるかも見ておきます。

測定環境は、

  • AMD Ryzen 7 5700X
  • DDR4-3200 32GB x 2
  • Linux kernel 6.4.13 (Debian 6.4.13-1)
  • GCC 13.2.0 (Debian 12.2.0-14)
  • glibc 2.37 (Debian 2.37-7)

では順に測定しましょう。

単純なFizzBuzzの速度
# fizzbuzz_simple.c

33.3GiB 0:01:41 [ 335MiB/s] [ <=>                                              ]

real    1m41.715s
user    1m38.481s
sys     0m31.222s
printf排除FizzBuzzの速度
# fizzbuzz_myitoa.c

33.3GiB 0:00:22 [1.48GiB/s] [                     <=>                          ]

real    0m22.478s
user    0m14.279s
sys     0m11.151s
9桁10桁狙い撃ちのFizzBuzzの速度
# fizzbuzz_9_10.c

33.3GiB 0:00:08 [4.12GiB/s] [        <=>                                       ]

real    0m8.080s
user    0m3.138s
sys     0m12.550s
vmspliceを使ったFizzBuzzの速度
# fizzbuzz_vmsplice.c

33.3GiB 0:00:03 [10.6GiB/s] [   <=>                                            ]

real    0m3.159s
user    0m2.828s
sys     0m1.654s

基本的にPentiumで有効な高速化手法はRyzenでも有効ですが、効き目という観点で見ると違いがあります。Ryzenの場合、自作アルゴリズムの要である9桁10桁の狙い撃ちがあまり効かないようです。

オフセット0xf6アルゴリズム(仮)も測定しましょう。昨日のコードから少し変更しているのでPentiumでも測りなおします。

オフセット0xf6のFizzBuzzの速度(Pentium J4205)
# https://github.com/katsuster/fizzbuzz/blob/main/fizzbuzz2.c

33.3GiB 0:00:09 [3.40GiB/s] [         <=>                                      ]

real    0m9.789s
user    0m7.660s
sys     0m4.847s
オフセット0xf6のFizzBuzzの速度(Ryzen 7 5700X)
# https://github.com/katsuster/fizzbuzz/blob/main/fizzbuzz2.c

33.3GiB 0:00:02 [15.3GiB/s] [  <=>                                             ]

real    0m2.184s
user    0m1.827s
sys     0m1.422s

自作アルゴリズムとオフセット0xf6アルゴリズム(仮)を比べると、Pentium J4205の場合はさほど差はありませんでしたが、Ryzen 7の場合は1.5倍程度と大きく差がついています。理由は良くわかりませんが、自作アルゴリズムの方にRyzen 7が苦手とする処理があるのでしょう。

FizzBuzzの種類Pentium J4205の実行時間倍率Ryzen 7の実行時間倍率
単純 7m32.741s- 1m41.715s-
printf排除 1m20.416sx5.6 22.478s x4.5
9桁10桁狙い撃ち25.372s x17.8 8.080s x12.5
vmsplice 10.543s x42.9 3.159s x32.1
オフセット0xf6 9.789s x46.2 2.184s x46.5

まとめるとこんな感じです。最初(2023年9月21日の日記)にレギュレーションのところで説明したように、1から2^32-2まで(約42億回)FizzBuzzしているのですが、たった2秒で終わってしまいます。Ryzen速いですね……。

編集者:すずき(2024/01/23 10:51)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2023年9月22日

FizzBuzzを速くする2(高速アルゴリズムの紹介)

目次: ベンチマーク

FizzBuzzの実装は簡単ですが、可能な限り高速に出力しようとするとなかなか面白い遊びになります。前回は自作のアルゴリズムを紹介したので、今回は他の方が開発した高速化手法を紹介したいと思います。名前がないようなので、オフセット0xf6アルゴリズム(仮)と呼ぶことにします。

前回の最速(9桁10桁狙い撃ち+vmsplice)も含めて、ソースコードはGitHubに置いています(GitHubへのリンク)。

整数演算と文字列変換のトレードオフ

FizzBuzzの高速化の難しい点は、数値のインクリメント(=1ずつ増やす)と数字を文字列に変換する処理の両立です。単純な方法としては、現在の数値を整数で保持する方法、文字列で保持する方法が考えられます。

  • 整数で保持: 演算は高速、文字列への変換は低速
  • 文字列で保持: 演算は低速(繰り上がり処理にループが必要で遅い)、文字列変換は不要なので最速

どちらも一長一短で困りました。

整数演算と文字列変換の両立

このトレードオフを見事に解決しているのがオフセット0xf6アルゴリズム(仮)です。最初に要点を列挙しますと、

  • 1バイト1桁(例えば64ビット変数なら8桁収まる)
  • 各桁を0xf6でオフセット(0: 0xf6, 1: 0xf7, ..., 9: 0xff)
  • 桁上がりするまでは数値のインクリメントは整数演算
  • 桁上がりすると下の桁が全部0になるので、Trailing Zeroとマスク演算で0xf6をセットし直す

桁は1つ目に書いた通り、10進数1桁を1バイトで表現します。情報量としては過剰に見えますが文字列変換との両立のためです。


桁とバイト表現

数の表現ですが0 = 0xf6, 1 = 0xf7, 2 = 0xf8, ... 9 = 0xffとします。10進数の123397ならば0xf6f6f7f8_f9f9fffdとなります。


10進数と相当するバイト表現

桁上がりするまで数値インクリメントは+1の整数演算で実現できます。


インクリメント処理は+1の加算処理と同等

ここまではオフセットが変なだけの普通の整数です。

桁の繰り上がり処理

このアルゴリズムは桁の繰り上がり処理がエレガントで、+1の整数演算で適切な桁までの繰り上げが発生します。つまり加算命令1発で良く、ループ処理は必要ありません。先の例では下位の3桁が399から400になります。


桁の繰り上がりが発生した様子

面倒な処理は不要で、+1の整数加算だけで、1の桁より上位の桁(この例だと10の桁も)が全て正しく繰り上がります。このアルゴリズムのナイスポイントその1です。

繰り上がった桁は値0x00になるので手当が必要です。先の例だと12399: 0xf6f6f7f8_f9f9ffffに+1すると、124xx: 0xf6f6f7f8_f9fa0000になって、下位の2桁(10の桁、1の桁)が無意味な値になっています。次の演算を行うには0x00の部分を10進数の0を意味する0xf6に戻す必要があります。

戻し方はまずCTZ (Count Trailing Zeros)で下位の連続している0のビット数を取得します。先の例ですと、下位24ビットが0xfa0000 = 0b1111_1010_0000_0000_0000_0000ですので、17ビットです。Nとします。


0xf6をセットすべきバイト範囲の計算

CTZビット数Nを8の倍数に切り下げ(= 16ビット)て、mask = (1 << N) - 1とし、下位16ビットに1がセットされた(= 0x00000000_0000ffff)マスクを作成します。その後(元の数値) |= 0xf6f6f6f6_f6f6f6f6 & maskを計算して、下位16ビットに0xf6をセットします。


下位の連続した0x00のバイトのみに0xf6をセット

これで桁の繰り上げ処理は完了です。ループ処理は一切不要。アルゴリズムのナイスポイントその2です。

CTZにループが必要では?と思われるかもしれませんが、世の中には素敵なアルゴリズムがあってループなしで計算可能です。また現代のCPUはCTZ専用命令を持っていることが多く、基本命令の組み合わせより高速に処理できることが多いです。

文字列への変換

桁の繰り上がり処理の素晴らしさが伝わったところで、文字列への変換を紹介します。といっても極めて単純で高速です。0xc6c6c6c6_c6c6c6c6を減算するだけです。

一見すると意味不明ですが、ASCIIコードを考えるとわかると思います。0を表すバイト表現は0xf6でした。0xc6を引くと0xf6 - 0xc6 = 0x30になります。'0'はASCIIコードで0x30です。それだけで文字の'0'に変換できてしまいます。0以外の数値はどうなるかというと、

  • 0: 0xf6 - 0xc6 = 0x30 = '0'
  • 1: 0xf7 - 0xc6 = 0x31 = '1'
  • 2: 0xf8 - 0xc6 = 0x32 = '2'
  • ...
  • 9: 0xff - 0xc6 = 0x39 = '9'

となります。他の位置のバイトも同様で、減算1回で8桁を8文字に変換できます。このアルゴリズムのナイスポイントその3です。

アルゴリズムとは関係ないですが、文字列をメモリに書くときはエンディアンに注意です。x86系CPUはリトルエンディアンなので、文字列に変換した64bit変数(0x30303132_33343938)をそのままメモリに書くと順序が逆転し、メモリには0x38 0x39 0x34 0x33 0x32 0x31 0x00 0x00、つまり"89432100"になります。

本当は"00123498"と書いてほしいので、メモリに書く前にビッグエンディアンに変換すれば良いです。この処理はバイトスワップと呼ばれたりします。これもループ不要の処理で、現代のCPUだと専用命令を持っている場合もあります。

以上がオフセット0xf6アルゴリズム(仮)のナイスポイントの紹介でした。いやあ、良く考え付いたなこれ。感心しました。

エレガントで速い

遅くなる要素は見当たりませんが、最後に測定しましょう。

オフセット0xf6のFizzBuzzの速度
# https://github.com/katsuster/fizzbuzz/blob/main/fizzbuzz2.c

33.3GiB 0:00:09 [3.39GiB/s] [         <=>                                      ]

real    0m9.824s
user    0m7.447s
sys     0m5.064s

約45倍まで速くなりました。素晴らしいです。

参考までに、前回私が作成した9桁10桁狙い撃ちの力業アルゴリズム(約42倍)はこのくらい。

9桁10桁狙い撃ちのFizzBuzzの速度
# https://github.com/katsuster/fizzbuzz/blob/main/fizzbuzz.c

33.3GiB 0:00:10 [3.16GiB/s] [          <=>                                     ]

real    0m10.543s
user    0m8.921s
sys     0m4.067s

ボロ負けというほど差は付いていませんが、コードのエレガントさは大いに差がありましたね。当たり前ですが、リングバッファやvmsplice()のような共通して使える工夫は双方で使いました。ですから純粋にFizzBuzz最適化アルゴリズムの差と言えましょう。

編集者:すずき(2024/01/23 10:51)

コメント一覧

  • hdkさん(2023/09/23 14:56)
    +1だから、繰り上がる時は必ず下のほうに0が固まるんですね。だから、CTZすらなくても、ビット演算と引き算などを組み合わせれば、分岐なしで、0になったところを0xf6で埋められるという... 賢い! Packed (8ビットで2桁分) でも同じ作戦はいけそうですね、ASCIIに変換するのが面倒にはなりますが。
  • すずきさん(2023/09/23 21:14)
    そうなんですよ。賢いなーと思って自分でも実装してみて、日記に書きました。桁の繰り上がりもさることながら、ASCIIへの変換が凄く速いのが特徴ですね。
open/close この記事にコメントする



link もっと前
2023年10月1日 >>> 2023年9月22日
link もっと後

管理用メニュー

link 記事を新規作成

<2023>
<<<10>>>
1234567
891011121314
15161718192021
22232425262728
293031----

最近のコメント5件

  • link 24年4月22日
    hdkさん (04/24 08:36)
    「うちのHHFZ4310は15年突破しまし...」
  • link 24年4月22日
    すずきさん (04/24 00:37)
    「ちゃんと数えてないですけど蛍光管が10年...」
  • link 24年4月22日
    hdkさん (04/23 20:52)
    「おお... うちのHHFZ4310より後...」
  • link 20年6月19日
    すずきさん (04/06 22:54)
    「ディレクトリを予め作成しておけば良いです...」
  • link 20年6月19日
    斎藤さん (04/06 16:25)
    「「Preferencesというメニューか...」

最近の記事3件

  • link 24年4月25日
    すずき (04/26 16:49)
    「[AVIFの変換] AVIFが読めないアプリケーションがたまにあるので、AVIF(AV1 Image File Format)...」
  • link 24年2月7日
    すずき (04/24 02:52)
    「[複数の音声ファイルのラウドネスを統一したい] PCやデジタル音楽プレーヤーで音楽を聞いていると、曲によって音量の大小が激しく...」
  • link 24年4月22日
    すずき (04/23 20:13)
    「[仕事部屋の照明が壊れた] いきなり仕事部屋のシーリングライトが消えました。蛍光管の寿命にしては去年(2022年10月19日の...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDFファイル RSS 1.0

最終更新: 04/26 16:49