C++17の新機能を試す〜その1「3次元版hypot」

2018/02/14
このエントリーをはてなブックマークに追加


はじめに

カブクの甘いもの担当の高橋憲一です。

カブクでは3Dモデルデータの各種解析をしたり、プレビュー用画像のレンダリングをするエンジンを高速実行するためにC++で開発しています。私が2014年にカブクに入って3Dモデル解析エンジンの開発を始めた当時はまだC++11を使い始めたばかりでしたが、早いものでC++の規格はC++14、そしてC++17と進化してきました。

C++17は2017年に規格が制定されたものですが、かなりの部分の機能が使えるようになってきたので、今後書いていくコードにどのように取り入れていくか少しずつ見ていきたいと思います。

C++今昔

私が始めてC++を触ったのは最初の規格であるC++98が制定される前、


for (int i = 0; i < 10; i++)

この書き方すらできなかった頃です(正確にはgccは対応しているけど別のベンダー純正のコンパイラは対応していないとか、そんな状況でした)。
どういうことかというと、ループのカウンタである変数の int i を for のカッコの初期化のところで宣言し、for ループのスコープの中でのみ有効な変数とするということができなかったのです。今となっては信じられないほどですが...


int i;
for (i = 0; i < 10; i++)

というように書かなければなりませんでした。
(まだC++プログラマとして駆け出しの頃で、当時むさぼるように読んでいた書籍「Effective C++」のサンプルコードの文字列に"Hello World"の代わりにnuqneHとクリンゴン語で書いてあるのを見つけて喜んでいたことを思い出します。)

月日は経ち、C++11では


std::vector<int> hogehoge;
int sum = 0;
for (auto a: hogehoge) {
    sum += a;
}

というようなループも書けるようになったことには隔世の感がありました。

閑話休題。昔話はこれくらいにして...
綺麗なコード、良いコード、堅牢なコードを書いていくためには新しい機能もどんどん取り入れていきたいところです。

3次元hypot

https://ezoeryou.github.io/cpp17book/ 辺りを見ていると実にたくさんの追加機能がありますが、今回は3次元版hypotを見てみます。
これまでもhypot(x, y)関数は存在しましたが、C++17でx, y, zの3つの要素の引数を受け取る3次元版が追加されました。
いわゆる三平方の定理、

$$\sqrt{X^2+Y^2+Z^2}$$

を計算してくれるものです。カブクで開発、運用しているしている3Dモデル解析&レンダリングエンジンではこのような3次元ベクトルのX, Y, Zの各要素の合成成分、すなわちベクトルの長さを求めるような計算は至る所に出てきます。
もちろん、3次元ベクトルを保持、計算するクラスは独自に実装したものを使っているのですが、もしかしたら高度に最適化された実装が施されていて自前の実装より良いパフォーマンスを叩き出してくれるのではないかという淡い期待をもちつつ、これを使うことによるコードの変化と実行時のパフォーマンスを比較してみたいと思います。

現在のコードを抜粋すると下記のような感じになります。


#include <cmath>

class Vector3D {
public:
    Vector3D(double x, double y, double z);
    double length();

private:
    double x, y, z;
};

double Vector3D::length()
{
    return sqrt(x * x + y * y + z * z);
}

このlength()のメンバ関数の中をhypotを使って書くと


double Vector3D::length()
{
    return std::hypot(x, y, z);
}

となり、わずかではありますが見た目はシンプルになります。
実行時のパフォーマンス計測は、それぞれのstd::hypotを使った場合と、使わなかった場合をそれぞれ値を少しずつ変えながら、差を分かりやすくするために以下のようなコードで1億回実行した際の時間を計測しました。


double getSec()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + tv.tv_usec * 1e-6;
}

int main()
{
    Vector3D vec(1.0, 2.0, 3.0);
    double s = getSec();
    const int loopNum = 100'000'000;
    double length;
    for (int i = 0; i < loopNum; i++) {
        vec.x = i;
        length = vec.length();
    }
    double e = getSec();
    std::cout << loopNum << " times hypot --- " << e - s << "[sec]" << std::endl;
}

ここでloopNumに代入している100'100'000という書き方ははC++14から導入されていたもので、区切り文字を入れることで数値の桁を見やすくしてくれます。実際の値には影響はありません。

hypot使用、不使用による実行時間の計測結果

実行時間(秒)
最適化あり (-O2) 最適化なし
hypot使用 0.363832 4.04141
hypot不使用 0.08293 1.44227



実行環境 MacBook Pro 15inch (2017)
- CPU Core i7 2.9GHz
- メモリ 16GB
- OS macOS Sierra上のDockerでUbuntu 17.04を実行
- コンパイラ gcc 7.0.1
- 最適化 (-O2) ありと最適化なしの場合に分けて計測

最適化ありの場合のコンパイルオプション


g++ -std=c++17 -O2

結果はhypotを使わない場合の方が速く、その差は約4倍という開きがあります。
なぜそのような結果になるのかgccでの実装を見てみると

https://gcc.gnu.org/git/ より)


template<typename _Tp>
    inline _Tp
    __hypot3(_Tp __x, _Tp __y, _Tp __z)
    {
    __x = std::abs(__x);
    __y = std::abs(__y);
    __z = std::abs(__z);
    if (_Tp __a = __x < __y ? __y < __z ? __z : __y : __x < __z ? __z : __x)
        return __a * std::sqrt((__x / __a) * (__x / __a)
                            + (__y / __a) * (__y / __a)
                            + (__z / __a) * (__z / __a));
    else
        return {};
    }

inline double
hypot(double __x, double __y, double __z)
{ return std::__hypot3<double>(__x, __y, __z); }

各要素を二乗してsqrtを取ることに変わりはないのですが、
まず、各要素の絶対値を求めて、最も大きな要素を係数aとして0除算を考慮しつつ各要素を割ってから2乗して平方根を取り、再びaを掛けて戻す、ということをしています。こうすることによって、絶対値が大きい値を2乗することで起こりうる浮動小数点の指数部の桁あふれを防ぐための対策がなされており、高パフォーマンスのためではなく、より精度の高い値をもとめるための実装だということが分かります。

例えば2の512乗という値は、それ自体はdoubleで扱える範囲の値ですが2乗するとinf(桁あふれ)を起こします。


double x = 0x1p512;
Vector vec(x, 2.0, 3.0);
std::cout << vec.length() << std::endl;


というコードを実行すると、
- hypotを使った場合 ... 2.3223e+154
- hypotを使わない場合 ... inf
という結果になります。
(ここで x に代入している 0x1p512 は 1 * 2の512乗を表しています。これはC++17で追加された16進数浮動小数点数リテラルで、16進で仮数部の値を書いた後、pに続いて10進で指数部の値を書きます)

まとめ

というわけで、

3次元版hypot ... 現時点ではエンジンの実装には使わない
- 精度の高い計算結果は魅力的だが、速度重視のポリシーを捨ててまで採用するほどではない
- 今後の実装がどう変化していくか引き続きチェックは続ける

という結論になりました。
何事も試してみないとわからないことがあります。

こんな風にいろいろ試行錯誤しながらC++で高速実行するためのコードをガリガリ書きたいというエンジニアの方がいましたら、ぜひお知らせください。

その他の記事

Other Articles

2022/06/03
拡張子に Web アプリを関連付ける File Handling API の使い方

2022/03/22
<selectmenu> タグできる子; <select> に代わるカスタマイズ可能なドロップダウンリスト

2022/03/02
Java 15 のテキストブロックを横目に C# 11 の生文字列リテラルを眺めて ECMAScript String dedent プロポーザルを想う

2021/10/13
Angularによる開発をできるだけ型安全にするためのKabukuでの取り組み

2021/09/30
さようなら、Node.js

2021/09/30
Union 型を含むオブジェクト型を代入するときに遭遇しうるTypeScript型チェックの制限について

2021/09/16
[ECMAScript] Pipe operator 論争まとめ – F# か Hack か両方か

2021/07/05
TypeScript v4.3 の機能を使って immutable ライブラリの型付けを頑張る

2021/06/25
Denoでwasmを動かすだけの話

2021/05/18
DOMMatrix: 2D / 3D 変形(アフィン変換)の行列を扱う DOM API

2021/03/29
GoのWASMがライブラリではなくアプリケーションであること

2021/03/26
Pythonプロジェクトの共通のひな形を作る

2021/03/25
インラインスタイルと Tailwind CSS と Tailwind CSS 入力補助ライブラリと Tailwind CSS in JS

2021/03/23
Serverless NEGを使ってApp Engineにカスタムドメインをワイルドカードマッピング

2021/01/07
esbuild の機能が足りないならプラグインを自作すればいいじゃない

2020/08/26
TypeScriptで関数の部分型を理解しよう

2020/06/16
[Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

2020/03/19
[Web フロントエンド] Elm に心折れ Mint に癒しを求める

2020/02/28
さようなら、TypeScript enum

2020/02/14
受付のLooking Glassに加えたひと工夫

2020/01/28
カブクエンジニア開発合宿に行ってきました 2020冬

2020/01/30
Renovateで依存ライブラリをリノベーションしよう 〜 Bitbucket編 〜

2019/12/27
Cloud Tasks でも deferred ライブラリが使いたい

2019/12/25
*, ::before, ::after { flex: none; }

2019/12/21
Top-level awaitとDual Package Hazard

2019/12/20
Three.jsからWebGLまで行きて帰りし物語

2019/12/18
Three.jsに入門+手を検出してAR.jsと組み合わせてみた

2019/12/04
WebXR AR Paint その2

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く