Files
information-processing-1_10…/section/appendix.tex
2025-06-24 21:42:02 +09:00

107 lines
6.2 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\appendix
\section{付録}
\subsection{Deep Dive - if と switch の根本的な違い}
初学者は\texttt{if}文と\texttt{switch}文の使い分けで困まる時があるようだ。
前述したとおり、どちらも条件分岐を行うことができる。
しかし、2つには文法以外に明確な違いがある。それは、\texttt{switch}文のほうが「\textbf{圧倒的}」に\\処理速度が速いというところである。
まず、その処理速度の違いを見てみる。
以下のPythonスクリプトで10万個の条件文を含むCソースコードを生成する
\defaultlistingstyle
\lstinputlisting[language=Python,title={\texttt{if}文10万個}]{../programs/ifvswitch/gen-if.py}
\lstinputlisting[language=Python,title={\texttt{switch}文10万個}]{../programs/ifvswitch/gen-switch.py}
これらを実行し、Cソースコードを生成する
\begin{lstlisting}
$ python gen-if.py
$ python gen-switch.py
\end{lstlisting}
\newpage
以下が生成されたソースコードの一部始終:
\lstinputlisting[language=C,title={\texttt{if}文10万個のソースコード},consecutivenumbers=false,linerange={1-8,199996-200010}]{../programs/ifvswitch/ifs.c}
\lstinputlisting[language=C,title={\texttt{switch}文10万個のソースコード},consecutivenumbers=false,linerange={1-8,99998-100011}]{../programs/ifvswitch/switch-case.c}
\begin{lstlisting}
// 検証に影響する最適化を無効にしてコンパイルする
$ gcc -O0 ifs.c -o ifs
$ gcc -O0 switch-case.c -o switch-case
\end{lstlisting}
生成物をタイマに通して実行する:
\begin{lstlisting}
$ time ./ifs
real 0m59.045s
user 0m58.444s
sys 0m0.001s
$ time ./switch-case
real 0m0.006s
user 0m0.004s
sys 0m0.002s
\end{lstlisting}
実行結果から\texttt{switch}文の方が\texttt{if}文の約9840倍も高速であると分かる。
では、なぜ\texttt{switch}文の方が速いのか。それは、比較演算の数にある。
\texttt{if}文は記述した数と同じ数の比較演算が使われている。
一方、\texttt{switch}文は比較に関する演算は一度しか行われない。
それぞれのバイナリをIntel記法\cite{intel_manual}で逆アセンブルする:
\begin{lstlisting}
$ objdump -Mintel -d ifs
$ objdump -Mintel -d switch-case
\end{lstlisting}
\lstinputlisting[numbers=none,title={\texttt{if}文10万個の\texttt{comp}関数の逆アセンブル(抜粋)}]{../programs/ifvswitch/ifs-dump-trunc}
\lstinputlisting[numbers=none,title={\texttt{switch}文10万個の\texttt{comp}関数の逆アセンブル(抜粋)}]{../programs/ifvswitch/switch-case-dump-trunc}
\texttt{if}文では\texttt{cmp}\texttt{jne}\texttt{mov}\texttt{jmp}が並んでおり、上から順に値を比較していき、等価が確認されたらアドレス\texttt{0x5d0d47}に飛び関数から抜け出る。
この時、値が大きいと関数の深部まで進めないと等価が確認されないので時間がかかる。言い換えれば比較回数は引数の値に比例するといえる($O(n)$)。
一方\texttt{switch}文では、\texttt{cmp}\texttt{jne}のような比較命令は関数の始め以外全く見あたらない。
ここで、関数の始めの10数個の命令を見ると、4行目では16進数\texttt{0x1869f}$(99999)$とスタック上にロードされた引数を比較している。
次の\texttt{ja}命令では前行の結果に応じて関数の終わり付近のアドレス\texttt{0x4f5344}へ飛ぶ。
この命令は「○○以上であるか」を検証するので、この場合は等価検証する値の範囲である0〜99999に引数が含まれているかを検証し、そうでなければその時点で関数から抜け出る処理を行っている。
13行目まではアドレスに関する算術を行っている。
\texttt{rdx}レジスタには引数の4倍の値を代入し、\texttt{rax}レジスタには\texttt{0x4f6004}というマジックナンバーをロードする。
このマジックナンバーは整数型配列のアドレスとなっている。
この配列は更にはコンパイル時に計算されたマジックナンバーがリトルエンディアンで格納されてる。
次に32ビットの\texttt{eax}レジスタに\texttt{rdx}レジスタと\texttt{rax}レジスタの和が示めすアドレスが指している値を代入している。
つまり、引数をインデックスとし、それに32ビット整数型のサイズである4バイトを掛け、配列のアドレスに足すことで配列にアクセスしている。
例として引数が2の時、アドレスは$\texttt{0x4f6004} + \texttt{0x4} * \texttt{0x2} = \texttt{0x4f600c}$となり、そこから4バイト分を読むと\texttt{4db1f0ff}となり、リトルエンディアンとして変換すると\texttt{0xfff0b14d}となる。
\texttt{cdqe}命令は\texttt{eax}レジスタをサイズが倍の\texttt{rax}レジスタに符号拡張している。
この時\texttt{rax}レジスタには負の値が入っている。
引数が2の時、\texttt{0xfff0b14d}を符号拡張すると\texttt{0xfffffffffff0b14d}となり、この数の2の補数で負の数と解釈すると\texttt{-0xf4eb3}となる。
\texttt{rdx}レジスタに\texttt{0x4f6004}を再びロードし、\texttt{rax}レジスタに\texttt{rdx}レジスタの値を加算する。
引数が2の場合、$\texttt{-0xf4eb3} + \texttt{0x4f6004} = \texttt{0x401151}$となる。
この後\texttt{rax}レジスタに代入されているアドレスに飛ぶが、この時のアドレスは\texttt{case}ラベルで指定した処理に対応している。
これはハッシュマップに似たもので、どんな値でも処理速度は変化することはない($O(1)$)。
この魔法のような演算のおかげで、\texttt{switch}文は比較せずとも分岐を行うことができ、\texttt{switch}文で扱える値の型が整数型なのも納得いくだろう。
結局\texttt{if}文と\texttt{switch}文どちらを使うべきかは分岐の数と実行環境による。
分岐の数が少なく、処理速度はあまり求められない場合は\texttt{if}文、膨大な分岐を必要とし、かつ処理速度に制限がある場合は\texttt{switch}文が有用である。