ビット演算でダブル型数値を変換―浮動小数点乗算と加算のみで実現(2020年)

2026/01/25 23:55

ビット演算でダブル型数値を変換―浮動小数点乗算と加算のみで実現(2020年)

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

この記事では、IEEE‑754 の倍精度浮動小数点数を、その 64 ビット整数表現に変換したり逆に変換したりする方法を、ビット演算命令や明示的なキャストを一切使わず、加減乗除と比較という「浮動小数点演算」のみで実現する手法を紹介します。
この方法は標準の「四捨五入( ties to even)」に依存しており、コンパイラが unsafe な数値最適化(例:

/fp:fast
)を行った場合やシェーダーコンパイラが精度を落とす場合には失敗します。NaN・Infinity・‑Infinity を区別できず、加減乗除のみで負のゼロは正のゼロとして扱われます。

基本ブロック

  • double_and
    double_not
    double_or
    といった単純な論理関数(すべて倍精度浮動小数点で実装)
  • 条件付き選択を行う
    select
  • 比較 (
    x==0
    ,
    x<y
    ) は「最小サブノーマルを加えるときの ULP 動作」を利用した
    adding_smallest_is_precise
    テストから構築

指数部の抽出

  1. 数値を 2 のべき乗で繰り返しスケーリング (
    p2(-test)
    )
  2. スケール後の指数が 0 か 1 かを判定し、ビン検索で符号化された指数を倍精度浮動小数点として取得

仮数部の抽出

  • 数値を指数 0 に正規化し、暗黙的な先頭 1 を除去(サブノーマルの場合は除外せず)
  • 次にスケーリングして仮数が整数範囲に収まるようにし、倍精度浮動小数点内で整数として扱う

軽量な「floor」テクニック

small_positive_floor
[0, p2(52)]
の値を最も近い整数へ丸めるために
p2(52)
を足し引きする。
変換関数は
double_as_uint32s
uint32s_as_double
の 2 ペアで示され、元々は約 5,000 の算術命令を含んでいたが、最適化後に一般的な floor をバイアスベースの
dirty_floor
に置き換え、多くの代数同値式を簡略化することで約 368 命令へと縮小

今後の展望

  • 単精度への拡張
  • 他の丸めモード(例:切り捨て、切り上げ)のサポート
  • 最小実装の作成
  • 倍精度関数に対して「算術のみ」でどこまで計算可能かを探求

この手法はビット演算が利用できないハードウェア(例:一部 GPU シェーダーや制限付き組み込みシステム)で特に有用です。コンパイラ、数値ライブラリ、およびハードウェアベンダーが言語の制約を破らずに生データビット列を公開する必要がある場合に役立ちます。

Text to translate

(The original text was provided in the user message.)

本文

トム・レイザーの言葉でいうと、
「これは完全に無意味ですが、いつかあなた方のうち誰かが少し奇妙な状況で役立つかもしれません。」


問題設定

以下のような環境を想定します。

  • 使える型は IEEE‑754 の倍精度浮動小数点(「double」)だけです。
  • そのビット表現に直接アクセスできる操作(C++ のビットキャスト、JavaScript の
    DataView
    等)はありません。

このとき、ある double を 2 個の符号なし 32‑bit 整数(いずれも double として保持)へ変換したり、逆にそれらを組み合わせて元の double に戻したりすることが必要になります。

こうした問題は時折出てきます。そこで興味深かったのは、「浮動小数点演算だけで(加減・乗除・ビット操作・分岐・指数計算・除算・比較を使わずに)」実装できるかということです。

実際、倍精度から整数への変換は「二分探索」的手法で行えます。
まず指数部をビット単位で確定し、その後小数部(仮数)も同様に取得します。ただし、乗算と加算だけで実現できるか? という疑問は最初は非現実的に思えていましたが、結局 はい。ただし、いくつかの制約があります。

主な制約

  • NaN, Infinity, -Infinity は変換不可(ビット操作なしでは区別できないため)。
  • 負ゼロと正ゼロは区別不可。加減・乗算だけでは符号情報を保持できません。
  • コードは IEEE‑754 の「四捨五入、偶数への丸め」 を前提にしています。
    シェーダーコンパイラなどで
    /fp:fast
    といった高速化フラグが有効になっている環境では動作しません。

以下は実際の実装例です(冗談半分)。
ライブラリに埋め込まれたら面白くなるかもしれません。どんな言語でもポート可能です。

function double_as_uint32s(double) {
  // NaN, Infinity, -Infinity は扱えない。-0 は 0 と同じ扱い。
  var a = double, b, c, d, e, f, g, h, i, j, k, l, m,
      low, high;

  f=2.2250738585072014e-308+a; j=5e-324; b=j+f; b-=f; m=-5e-324; d=m+b;
  b=4.4989137945431964e+161; d=b*d; d=b*d; g=d*d;
  d=1.0; g=d-g; h=m+f; f=h-f; f=j+f; f=b*f; f=b*f; f*=f; f=d-f; f*=g;
  g=-2.2250738585072014e-308+a; h=j+g; h-=g; h=m+h;
  h=b*h; h=b*h; h*=h; h=d-h; c=m+g; c-=g; c=j+c; c=b*c; c=b*c; c*=c; c=d-c; c*=h;
  k=c*f; c=5.562684646268003e-309*a;
  g=j+c; g-=c; g=m+g; g=b*g; g=b*g; g*=g; g=d-g; h=m+c; h-=c; h=j+h; h=b*h; h=b*h; h*=h; h=d-h;
  g=h*g; h=a*g; g=d-g;
  c=g*c; g=1024.0*g; f=2.0+g; c+=h; h=7.458340731200207e-155*c; l=1.0000000000000002;
  g=l*h; g=m+g; e=j+g; e-=g; e=b*e; e=b*e; c=e*c; e=d-e; g=e*h; c=g+c;
  e=512.0*e; g=8.636168555094445e-78*c; e+=f; f=l*g; f=m+f; h=j+f; f=h-f; f=b*f;
  f=b*f; c=f*c; f=d-f; g=f*g; f=256.0*f; c=g+c; e=f+e; f=2.938735877055719e-39*c;
  g=l*f; g=m+g; h=j+g; g=h-g; g=b*g; g=b*... [snipped for brevity] ...

  return [low, high];
}

function uint32s_as_double(low, high) {
  var a = low, b = high, c, d, e, f, g, h, i, j, k, l, m;
  b=9.5367431640625e-07*b; f=-0.4999999998835847; c=f+b; g=4503599627370497.0;
  ... [snipped for brevity] ...
  return a;
}

背景:IEEE‑754 の倍精度

ここでは、後述のアルゴリズムを理解するために必要な double の構造と挙動を簡潔に説明します。
既にご存じであればスキップしてください。

double
は 64 ビットです。最上位ビットから順に

  1. 符号(1 bit)
  2. 指数部(11 bit)
  3. 仮数部(52 bit)

を持ちます。このビット列は「特別値」か「数値」に解釈されます。以下の擬似コード(無限精度、

**
はべき乗)で示します。

if (sign == 1) s = -1; else s = 1;

if (exponent == 0x7FF) {
    if (fraction == 0)   return NaN;
    else if (sign == 1)  return -Infinity;
    else                 return Infinity;
} else if (exponent == 0) {        // subnormal
    return s * (0.0 + fraction * (2 ** -52)) * (2 ** -1022);
} else {                           // normal
    return s * (1.0 + fraction * (2 ** -52)) * (2 ** (exponent-1023));
}

重要な性質

項目内容
符号付き最上位ビット0/–0 と NaN を除き、すべての数値に対して唯一の表現を保証します。
サブノーマル(非正規化)零に近づくにつれて表現可能な数の間隔が減少し、ULP が常に可算です。
整数ビット表現との順序性正の数では、64‑bit 整数表現を大きいほど浮動小数点値も大きくなる(単調増加)。

演算は「正確な数学的操作」として定義され、結果が表現できない場合に丸められます。
IEEE‑754 では複数の丸めモードがありますが、本稿で扱うのは 「最も近い値へ、偶数への丸め」(round to nearest, ties to even)です。


2.1 論理演算

加減乗算だけで次の論理プリミティブを構築できます。

constexpr double double_and(double a, double b) { return a * b; }
constexpr double double_not(double a)           { return 1 - a; }
constexpr double double_or (double a, double b){ return a + b - a*b; }

constexpr double select(double cond, double t, double f)
{ return cond*t + double_not(cond)*f; }

これらはすべて有限数に対して機能します。
Infinity や NaN を生成しないよう注意が必要です。


2.2 比較の構築

adding_smallest_is_precise(x)

最小正サブノーマル(

p2(-1074)
)を加えると変化するかどうかを判定します。
結果は 0.0 または 1.0 の真偽値として返ります。

is_exp_0_or_1(x)

前述のテストを

x
-x
に対して行い、指数部が 0(サブノーマル)か 1(正規化で最小指数)かを判定します。

is_zero(x)

非常に小さな魔法数を足し引きすることで、ゼロ を検出します。

cmp_sub(x, y)

x - y
の符号を保持したまま、Infinity になることなくスケール調整された値を返します。

is_equal(x, y)

cmp_sub
is_zero
を組み合わせて等価判定を行います。


2.3 指数部の抽出

constexpr double get_encoded_exponent(double v)
{
    double tmp = v;
    double e   = 0;

    for (int test = 1024; test >= 1; test /= 2) {
        double trial      = tmp * p2(-test);
        double too_small  = is_exp_0_or_1(trial);

        tmp = select(too_small, tmp, trial);
        e   += select(too_small, 0.0, (double)test);
    }

    return select(is_exp_0_or_1(v), double_not(is_exp_0(v)), e + 2);
}

このループは指数部をビット単位で減算し、最終的に 0, 1, 2 のいずれかになるまで続きます。

e
に減算量を蓄積しておくことで、実際の指数値が得られます。


2.4 符号と仮数部

is_less_than_zero(x)

正規化後に大きな定数を足すことで、負か非負かを判別します。

make_exp_0_or_1(v)

v
を指数が 0 または 1(あるいは 2)になるまでスケールダウンします。
指数が 2 の場合は半分にします。

get_fraction(v)

上記で得た正規化された値から暗黙の 1 を除き、52 ビット分だけ左シフトして仮数部を取得します。


2.5 Double → Unsigned 32‑bit 整数 2 個

struct low_high_doubles { double low; double high; };

constexpr low_high_doubles constexpr_double_as_ints(double v)
{
    double sign     = is_less_than_zero(v);
    double exponent = get_encoded_exponent(v);
    double fraction = get_fraction(v);

    // 上位 32 ビット: 符号 (bit 31)、指数 (bits 20‑30),
    // および仮数部の上位 11 ビット
    double high_fraction = small_positive_floor(fraction * p2(-32));
    double high = sign*p2(31) + exponent*p2(20) + high_fraction;

    // 下位 32 ビット: 残りの仮数ビット
    double low = fraction - high_fraction*p2(32);
    return {low, high};
}

2.6 Unsigned 32‑bit 整数 2 個 → Double

constexpr double double_from_sign_exp_fraction(
        double sign, double exponent, double fraction)
{
    // 指数がゼロでないか?
    double exp_is_non_zero = double_not(is_zero(exponent));

    // 仮数部を指数 0 に合わせる
    double v = fraction * p2(-1074);

    // 必要なら暗黙の 1 を足す
    v += select(exp_is_non_zero, p2(-1022), 0.0);

    // シフト量計算
    double e = select(exp_is_non_zero, exponent - 1, 0.0);
    e *= p2(-10);                // ビット抽出用に調整

    // 二分探索的指数復元
    for (int test = 1024; test >= 1; test >>= 1) {
        double cond = small_positive_floor(e);
        e = (e - cond)*2;
        if (test == 1024)
            v *= select(cond, p2(512), 1.0); // 未表現の p2(1024) を扱う
        else
            v *= select(cond, p2(test), 1.0);
    }

    // NaN の生成: 指数全ビットが 1 で仮数部がゼロでない場合
    double is_nan = double_and(is_equal(exponent, 2047),
                               double_not(is_zero(fraction)));
    v *= double_not(is_nan);

    // 符号適用
    v *= select(sign, -1.0, 1.0);
    return v;
}

最後に、2 個の整数から符号・指数・仮数部を抽出して上記関数へ渡します。

constexpr double constexpr_ints_as_double(double l, double h)
{
    double exp_and_sign = small_positive_floor(h * p2(-20));
    double sign         = small_positive_floor(h * p2(-31));
    double exponent     = exp_and_sign - sign*p2(11);
    double fraction     = (h - exp_and_sign*p2(20)) * p2(32) + l;

    return double_from_sign_exp_fraction(sign, exponent, fraction);
}

3. 最適化と「汚い」Floor

floor
関数は多くの命令を必要としますが、以下のように丸め誤差を利用して下げることが可能です。
0 〜 2³² の範囲で小数点以下 32 ビットまでの精度がある値に対して有効です。

constexpr double dirty_floor(double v)
{
    return v - (0.5 - p2(-33)) + (p2(52)+1) - (p2(52)+1);
}

その他、

is_exp_around_0_or_1
unsafe_is_exp_0_or_1
などのヘルパーも簡略化できます。


4. まとめ

上記実装は 「加算・乗算だけで」 任意の有限 double 同士の関数を表現できることを示しています。
ただし、以下の点に留意してください。

  1. NaN, Infinity, -Infinity は扱えない(ビット操作なしでは区別不可)。
  2. 負ゼロと正ゼロは区別できない
  3. IEEE‑754 の「四捨五入、偶数への丸め」 が前提です。
  4. シェーダーコンパイラ等で高速化フラグが有効な環境では動作しません。

それでも、多くの制限付き言語や constexpr 文脈などで「ビット表現を取得したい」という場面に対して、加減乗算のみで実装できるという点は興味深い発見です。
ぜひ、ご自身のプロジェクトに応用してみてください。

同じ日のほかのニュース

一覧に戻る →

2026/01/26 4:03

まず、私に関心を持っていただけるようにしてほしいです。

## Japanese Translation: **改訂要約:** 著者は、ウェブサイト、個人の「Me」セクション、ブログを組み合わせた新しいプラットフォームを立ち上げました。外部リソースへのリンクとファンサポート用のPatreonページが特徴です。今後の計画としては、さらに多くのブログ投稿を追加し、リンク統合を拡大し、場合によっては追加のPatreonティアを作成することがあります。これによりユーザーはキュレーションされたコンテンツへ簡単にアクセスでき、クリエイターには追加収益源が提供されます。 (このバージョンではすべての主要ポイントを保持し、推測された業界全体のトレンド表現を削除しています。)

2026/01/26 9:10

**科学者たちが「あなた」の限界を決める脳波を特定**

## Japanese Translation: > 本研究は、頭頂皮質におけるアルファ振動のリズムが、人々がゴム手を所有していると感じる強さを因果的に形成することを示しています。スウェーデンの研究者66名とフランスの研究者43名が合計106名の被験者から脳波(EEG)を記録し、古典的なゴム手錯覚を体験させました。ロボットアームは実際の手と偽の手の両方にタップを打ち、タップの同期性は最大500 msまで遅延させられました。 > > 被験者はタップが同期しているときに最も強い所有感を報告し、遅延が増すにつれてその感覚が弱まりました。EEGからは、頭頂部のアルファ周波数が被験者の遅延検知能力と相関していることが明らかになりました:高速なアルファ波は小さな時間ギャップに対する敏感性を高め、一方で低速なアルファ波は大きなギャップが存在しても錯覚を強化しました。 > > 頭頂部のアルファ波を人工的に高速化または遅延させる経頭蓋交流電流刺激(tACS)を用いて、研究者たちは因果関係を確認しました——アルファ波を加速すると所有感が増し、逆に減速すると錯覚が弱まり、自分の手と偽手との区別が難しくなりました。 > > これらの結果は *Nature Communications* に掲載され、頭頂部のアルファ活動が身体所有感を構築する上で因果的役割を果たすことを示唆しています。精神疾患(統合失調症や幻肢痛など)の治療に寄与したり、ターゲットとした脳刺激によってよりリアルな義手や仮想現実インターフェースの設計指針となる可能性があります。

2026/01/26 0:34

**タイトル** 「姿勢が悪くなると画面をぼかす macOS アプリ」

## Japanese Translation: Posturrは、MacのカメラとAppleのVisionフレームワークを使用してリアルタイムで姿勢を監視する軽量macOSアプリです。鼻と肩の角度を測定し、全身ランドマークが利用できない場合は顔検出にフォールバックします。前かがみが検知されると、画面が徐々にぼかれます(デフォルトではmacOSのプライベートCoreGraphics APIを使用し、互換モードではNSVisualEffectViewにフォールバック)。良い姿勢が回復するとぼかしは即座に消えます。ぼかしの強度は「Low」から「Very High」に段階的に上昇し、デッドゾーン設定で軽微な前かがみを無視できます。 Posturrは完全にローカルで動作します:動画データはマシンを離れず、オンラインアカウントやクラウドサービスも不要です。MITライセンスのソースコードはコミュニティへの貢献を歓迎しています。メニューバーには小さなアイコンがあり、ステータス表示、監視の有効/無効化、再校正、感度調整、互換モード切替、デッドゾーン設定、またアプリ終了などが可能です。また、外部制御用に`/tmp/posturr-command`というファイルベースのコマンドインターフェイス(`capture`、`blur <0‑64>`、`quit`)も公開しています。 インストールは簡単で、Homebrew(`brew install --cask posturr`)を使用するか、リリースページから署名済みのDMG/ZIPをダウンロードします。ノタリゼーションされており、システム設定でカメラ権限が必要です。最適な結果を得るためには、カメラを目線レベルに位置させ、十分な照明を確保し、画面を向いて座り続け、肩が見えるようにしてください。Posturrはマルチディスプレイ環境にも対応しており、macOS 13+でXcode Command Line Toolsを使用してソースからビルドできます。 すべての処理をデバイス上で完結させ、プライバシー優先設計を提供することで、Posturrは姿勢モニタリングツールがユーザーのプライバシーを侵害せずにエルゴノミック支援を行う方法を示しています。