1.3. 算術演算と精度

1. 加減乗除

コンピュータにおける四則演算は、数値の内部表現に応じて異なる特性を持ちます。ここでは整数演算と浮動小数点演算に分けて、それぞれの特徴と注意点を解説します。

1.1. 整数演算

1.1.1. 基本的な演算

整数の加減乗除は、一般的に正確な結果が得られますが、表現可能な範囲を超える場合にオーバーフローが発生します。

int a = 2147483647;  // int型の最大値(32bit符号付き)
int b = 1;
int result = a + b;  // オーバーフロー発生
// result = -2147483648 (予期しない負の値)

1.1.2. 除算の特殊性

整数除算では小数部が切り捨てられるため、数学的な除算とは異なる結果になることがあります。

int result1 = 7 / 3;    // 結果: 2 (2.333...の小数部切り捨て)
int result2 = -7 / 3;   // 結果: -2 (言語により-2または-3)

また、ゼロ除算は実行時エラーやプログラムクラッシュの原因となります。

1.1.3. 符号付きと符号なし整数の混在

符号付きと符号なしの整数を混在させると、予期しない結果を招くことがあります。

unsigned int a = 1;
int b = -1;
if (a > b) {
    // 実際にはこの条件は偽になる(bが大きな正の値として解釈される)
}

1.2. 浮動小数点演算

1.2.1. 基本的な特性

浮動小数点演算は近似値による計算のため、数学的に等しい式でも結果が異なることがあります。

double result1 = 0.1 + 0.2;  // 0.30000000000000004
double result2 = 0.3;        // 0.3
// result1 == result2 は偽

1.2.2. 演算順序の影響

浮動小数点演算では、演算順序により結果が変わることがあります。

double a = 1e20;
double b = 1.0;
double c = -1e20;

double result1 = (a + b) + c;  // 0.0
double result2 = a + (b + c);  // 1.0

1.2.3. 加減算での桁落ち

非常に近い値同士の減算では、有効桁数が大幅に減少する桁落ちが発生します。

double x = 1.0000001;
double y = 1.0000000;
double diff = x - y;  // 期待値: 0.0000001
// 実際は精度が低下し、誤差を含む結果となる

1.2.4. 乗除算の特徴

浮動小数点の乗除算は、指数部の加減算として実行されるため、比較的安定していますが、非正規化数や特殊値(無限大、NaN)の扱いに注意が必要です。

double result1 = 1.0 / 0.0;   // +∞(正の無限大)
double result2 = 0.0 / 0.0;   // NaN(非数)
double result3 = sqrt(-1.0);  // NaN

1.3. 演算における共通の注意点

1.3.1. アンダーフローとオーバーフロー

  • オーバーフロー: 計算結果が表現可能な最大値を超える
  • アンダーフロー: 浮動小数点で、結果が表現可能な最小の正の値より小さくなる

 3.5. オーバーフロー(あふれ)3.6. アンダーフローを参照してください。

1.3.2. 演算の交換法則・結合法則の破綻

コンピュータ演算では、数学的に成り立つ法則が必ずしも成り立ちません。

// 結合法則の破綻例
double a = 1e20, b = -1e20, c = 1.0;
double result1 = (a + b) + c;  // 1.0
double result2 = a + (b + c);  // 0.0

 3.2. 情報落ちを参照してください。

1.3.3. 累積誤差

繰り返し演算では誤差が蓄積し、最終的に大きな誤差となることがあります。

double sum = 0.0;
for (int i = 0; i < 10000; i++) {
    sum += 0.1;  // 理論値: 1000.0
}
// 実際の結果は1000.0から微小にずれる

2. 表現可能な数値の範囲

2.1. 8bit(符号なし整数)

 8bitを2進数で表現すると \(0000\) \(0000\) 〜 \(1111\) \(1111\) が表現可能となります。

 これを10進数で表現すると \(0\) 〜 \(255\) までの\(256\)個が表現可能です

2.2. 8bit(負の数を2の補数で表す)

 8bitのうち最上位ビットが符号として使われます。

 ゼロを含む正の数は2進数で表現すると \(0000\) \(0000\) 〜 \(0111\) \(1111\) となります。

 これを10進数で表現すると \(0\) 〜 \(2^8-1 = 127\) となります。

 負の数は2進数で表現すると \(1111\) \(1111\) 〜 \(1000\) \(0000\) となり10進数で表現すると \(-1\) 〜 \(-128\)となります。

 ですので10進数の \(−128\) 〜 \(127\) の\(256\)個が表現できます。

2.3. 16bit(符号なし整数)

 2進数で 0000 0000 0000 0000 〜 1111 1111 1111 1111 すなわち\(2^{16}-1\)となります。

 10進数にすると \(0\) 〜 \(65,535\) までの\(65,536\)個が表現できます。

2.4. 16bit(負の数を2の補数で表す)

 正の数は、2進数で \(0000\) \(0000\) \(0000\) \(0000\) 〜 \(0111\) \(1111\) \(1111\) \(1111\) となります。

 10進数にすると \(0\) 〜 \(2^{16}-1 = 32,767\)

 負の数は、2進数で \(1111\) \(1111\) \(1111\) \(1111\) 〜 \(1000\) \(0000\) \(0000\) \(0000\) となります。

 10進数にすると \(-1\) 〜 \(-1×2^{16} = -32,768\)となります。

 これを合わせて 10進数で \(-32,768\) 〜 \(32,767\) まで\(65,536\)個が表現できます。

2.5. 32bit(符号なし整数)

 上記と同様に計算すると、\(0\)から\(2^32\)まで、

 10進数で \(0\) 〜 \(2^{32} = 4,294,967,295\) までの\(4,294,967,296\)個が表現できます。

2.6. 32bit(負の数を2の補数で表す)

 こちらも上記と同様に計算すると、\(2^31\)から\(2^31-1\)まで、

 10進数で \(-2,147,483,648\) 〜 \(2,147,483,647\) までの\(4,294,967,296\)個が表現できます。

2.7. nbit(符号なし整数)

 10進数で \(0\) 〜 \(2^n\) までの\(2^n + 1\)個が表現できます。

2.8. nbit(負の数を2の補数で表す)

 10進数で \(-2^{n-1}\) 〜 \(2^{n-1} – 1\) までの\(2^n + 1\)個が表現できます。

bit 2の冪乗 最小 最大
8 \(2^8\) \(0\) \(255\)
16 \(2^{16}\) \(0\) \(65,535\)
32 \(2^{32}\) \(0\) \(4,294,967,295\)
n \(2^n\) \(0\) \(2^n\)
符号なし整数
bit 2の冪乗 最小 最大
8 \(-2{7}\)〜\(2^7-1\) \(-128\) \(127\)
16 \(-2{15}\)〜\(2^{15}-1\) \(-32,768\) \(32,767\)
32 \(-2{31}\)〜\(2^{31}-1\) \(-2,147,483,648\) \(2,147,483,647\)
n \(-2{n-1}\)〜\(2^{n-1}-1\) \(-2{n-1}\) \(2^{n-1}-1\)
負の数を2の補数で表す

3. シフト演算

 シフト演算は、ビット演算の一種で、バイナリデータ(ビット列)を左または右に指定されたビット数だけ移動させる操作を指します。

 シフト演算は、乗算や除算と比べて高速に実行できるため、特定の状況下での計算の高速化や、ビットフィールドの操作など、さまざまな用途で利用されます。

3.1. 論理シフト

 論理シフトは、各ビットを単純にシフトするだけで空いたビットには0を補充します。

 論理シフトは、例えば次のような使い方があります。

  • 乗算・除算の高速化
    • 左シフトは2のn乗での乗算と同じ効果を持ちます。例えば、\(x << 3\)は\(x * 2^3\)、つまり\(x * 8\)と同じです。
    • 右シフトは2のn乗での除算と同じ効果を持ちます。ただし、結果は整数として切り捨てられます。
  • ビットマスクの作成
    • 特定のビット位置に1をセットしたい場合や、特定のビットをクリアしたい場合に、シフト操作を使用してビットマスクを作成します。
  • データのパッキング・アンパッキング
    • 複数の小さなデータを1つの大きなデータにパックする場合や、その逆の操作を行う場合に、シフト操作を使用します。
  • ビットフィールドの操作
    • ビットフィールドは、1つの整数の中に複数の情報をビット単位で格納する方法です。シフト操作を使用して、特定のビットフィールドを読み取ったり、設定したりします。
  • エンディアンの変換
    • バイトオーダーを変更する場面で、シフト操作とビットのAND操作を組み合わせて使用します。
  • グラフィックスの操作
    • 画像データのピクセル値を操作する場合や、色の成分を抽出・設定する場合に、シフト操作を使用します。

3.2. 算術シフト

 算術シフトは、特に符号付き整数に対して行われるビットのシフト操作で、最上位ビット(符号ビット)が保持される特性を持っています。

 算術シフトは、例えば次のような使い方があります。

  • 符号付き整数の高速除算
    • 算術右シフトは、2のn乗での符号付き整数の除算と同じ効果を持ちます。例えば、\(x >> 2\)は\(x / 2^2\)、つまり\(x / 4\)と同じです。この操作は、浮動小数点の計算を避け、整数の計算のみで高速に除算を行いたい場面で利用されます。
  • 符号の拡張
    • 低ビット幅の符号付き整数を高ビット幅の整数に変換する際に、符号を正しく拡張するために算術シフトを使用します。例えば、8ビットの符号付き整数を32ビットの整数に変換する場合、最上位ビットを保持しながらシフトすることで、符号を正しく拡張できます。
  • 固定小数点数の操作
    • 固定小数点数は、小数部と整数部を一つの整数として表現する方法です。算術シフトを使用して、固定小数点数の乗算や除算を行うことができます。
  • データの正規化
    • データを特定の範囲に正規化する際に、算術シフトを使用してデータのスケーリングを行うことができます。
  • オーディオや画像の処理
    • オーディオデータや画像データのダウンサンプリングやアップサンプリングを行う際に、算術シフトを使用してデータの量子化を行うことができます。

4. 誤差

4.1. 桁落ち

 値がほぼ等しい二つの数値の差を求めたとき、有効桁数が減ることによって発生する誤差を桁落ちといいます。

 例えば、\(1.23456789\)と\(1.23456788\)の差を求めてみます。

 差は、\(0.00000001\)となり、元の有効桁数9桁から1桁へと有効桁数が減っています。

 浮動小数点数の場合、仮数部は正規化されます。そのため有効桁数が減ってしまうと減った分はすべて0で埋められます。これが誤差の原因となり最終的に大きな影響が出ることもでてきます。

 このようなことから桁落ちによる誤差は無視できないものでさまざまな対策が取られています。

4.2. 情報落ち

 絶対数が非常に大きな数値と小さな数値の加算や減算を行ったとき小さい数値が計算結果に反映されないことによって発生する誤差を情報落ちといいます。

 これは、浮動小数点数の仮数部が仮に23bitだった場合、大きな数と小さな数の加算や減算の結果が23bit以上ないと表現できない場合にあふれたbitが切り捨てられ、結果として小さい数値が無視されてしまうということです。

 例えば、\(1 + 1*2^{-24}\)の場合、\(1*2^{-24}\)は切り捨てられてしまいます。

 また、倍精度浮動小数点数を単精度浮動小数点数に変換しようとする場合にも桁数が減少することで情報落ちが発生する可能性があるため注意が必要です。

 情報落ちがしないようにするには、小さな数どうしの計算を先にすることで情報落ちしない桁数まで桁を上げ、そのあと大きな数との計算をするなどの工夫が必要です。

4.3. 丸め

 指定された有効桁数で演算結果を表すために切り捨て・切り上げ・四捨五入などで下位の桁を削除することによって発生する誤差を丸めによる誤差といいます。コンピュータで表現できる範囲におさめるための処理も丸めになります。

 例として、1を3で割った場合 \(1 / 3 = 0.333333\cdots \) を考えます。

 有効桁数を小数点以下8桁として 小数点以下9桁目移行を切り捨てると\(0.33333333\) となります。この場合どうなるでしょう。

 元の値を得るために得られた結果を3倍してみます。

 \(0.33333333 * 3 = 0.99999999\)

 元の値である \(1\)にならなく誤差 \(0.00000001\)が発生します。

 これが丸めによる誤差です。

4.4. 打切り

 無限級数で表される数値の計算処理を有限で打ち切ったことによって発生する誤差を打ち切り誤差といいます。

 打ち切り誤差は、計算を有限のステップで終了することによって生じる誤差のことを指します。テーラー展開を例にとると、この打ち切り誤差の概念がよく理解できます。

テーラー展開:
 関数( f(x) )が点( a )で無限回微分可能であるとき、その関数は以下のようにテーラー級数で表現できます。

 \( f(x) = f(a) + f'(a)(x-a) + \frac{f”(a)}{2!}(x-a)^2 + \frac{f”'(a)}{3!}(x-a)^3 + \cdots \)

 ここで、( f'(a) )は( f(x) )の点( a )での1階の導関数、( f”(a) )は2階の導関数、というように続きます。

 しかし、実際の計算ではこの級数を無限に続けることはできません。そのため、ある項までの和を取ることで近似的に関数の値を求めることが多いです。このとき、打ち切った項以降の級数の和が「打ち切り誤差」となります。

 例えば、テーラー展開を2次の項までで打ち切る場合、関数の近似式は以下のようになります。

 \( f(x) \approx f(a) + f'(a)(x-a) + \frac{f”(a)}{2!}(x-a)^2 \)

 このとき、3次の項以降が打ち切り誤差となります。

 このように、テーラー展開を用いると、関数の近似式を得ることができますが、項を打ち切ることで生じる誤差を打ち切り誤差と呼びます。この誤差は、打ち切る項の次数や、展開点( a )からの( x )の距離に依存します。

4.5. オーバフロー(あふれ)

 オーバーフローは、計算結果が表現可能な最大値を超えた場合に発生します。

 浮動小数点数で最大値を超える計算を行った場合、オーバーフローが発生し、結果は無限大 (Infinity-Infinity) として表現されます。

 32bitの浮動小数点数の場合、正の無限大は符号ビットが\(0\)、指数部が\(1111\) \(1111\)、仮数部が全て\(0\)、負の無限大は符号ビットが\(1\)、指数部が\(1111\) \(1111\)、仮数部が全て\(0\)となります。

 オーバーフローが発生すると、計算結果が不正確になる可能性があります。

4.6. アンダフロー

 アンダーフローは、計算結果が表現可能な最小値よりも絶対値が小さい場合に発生します。

 この場合、計算結果は非正規化数として表現されるか、またはゼロとして扱われます。

 アンダーフローは、指数部が全て0で、仮数部が非ゼロの場合に非正規化数として表現されます。また、それより小さな値の場合はゼロと表現されてしまいます。

 アンダーフローが発生すると、計算結果が不正確になるか、情報が失われる可能性があるため、アプリケーション側でどのように取り扱うかをしっかり検討しておく必要があります。