C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

キーボード: Azio Retro Classic Posh

美しいキーボードというのも魅力的なもので、 Azio Retro Classic Posh を Makuake で購入していたものが昨年末に届き、数日間使ってみた。

美しさについてのみ評価すれば100点満点とした上で85点。15点分の減点理由は次の通り:

  1. キーが樹脂部品でチープ感が否めない。銅製とは言わないが、せめてキーボード本体のフレームと同じ程度の金属感のある素材を期待していた。
  2. キーボード右上のインジケーターのLEDが内部で隣と仕切りなしに繋がっているようで光が漏れ安っぽさを感じる。

美しさについてはおおむね満足。

しかし、実用性についてはあまり良くない:

  1. キーがぐらつく。ほぼ垂直に打鍵しない限り、ぐらつきのために指の動きが安定しない。
  2. 抵抗が大きい。特に高速にタイプする際に引っ掛かりを大変強く感じ指の制御が乱れされてしまう。私は赤軸に慣れていることもあり、やや押下に強い力を必要とするため指が疲れる。カチ音などどうでもよいのでこのキートップとぐらつきならば赤軸かそれ以上に抵抗のないキースイッチを採用したバージョンがあるとよかったのではないか。
  3. ファンクションキー群が遠い。通常の手首の位置からでは指を伸ばしても素早く正確に打鍵できるほどには届かない。世間一般のユーザーにとってはファンクションキーは滅多に使わない飾りやマルチメディアキーになっているのかもしれないが、ゲーマーやプログラマー、あるいは文書書きであっても相応にファンクションキー群を使う事に慣れている方には「よいしょ」と手の位置をキーボード上部へシフトする必要があり疲れる。
  4. JIS配列なのに RETURN] の位置が英語配列。JISならJISにして欲しかった。
  5. 打鍵後の残響が安っぽい。ずしりとした音響ならともかく、樹脂部品がはねたような残響はチープでやや不快感の方が強いものだった。

メインのキーボードとして使うつもりで購入したが、私にはどうも実用上の都合が合わないところが大きいようで残念な買い物だったようだ。

Ultimate Hacking Keyboard が想定より開発が遅くまだ届かないところ、闇のキーボードに対して光のキーボードのような面白さも思い購入した Azio Retro Classics Posh であったが、 Ultimate Hacking Keyboard の到着を前に、キートップの禿げてしまった Corsair K60 へ戻そうか検討している。

慣れでどうにかなろうかと思い、もうしばらくは使ってみるつもりだが、いずれ、 Ultimate Hacking Keyboard が届けば倉庫行きになってしまいそうだ。 Azio Retro Classics Posh の購買層は美しさにある程度お金を出せる層だろうから、1つ5万円程度にコストアップしても、どうせならばもっと重厚で本格的な作りだとより嬉しかったかもしれない。

A Poem of "Hello, World!" using C++

0. はじめに

この記事は 初心者C++er Advent Calendar 2017 の DAY 24 に寄稿したものです。😃

概要

C++初心者向けの記事を書く」というお題のアドベンカレンダーがあったので、私が思いつく限りで兎にも角にも "Hello, World!" を出力する C++ を使ったコード例を淡々と紹介したいと思います。

Note: この記事は「かれんだー埋まらなさそー><」と思い、ふんわり書いたポエムです。途中からかるぅく怪しいわぁるどになりますのであんまり真面目に読まず、春はあけぼの的なゆるっとした気持ちで眺めて頂ければ幸いです。😏

1. いーじーもーど 編

よくあるやつ:

#include <iostream>
int main() { std::cout << "Hello, World!"; }
Hello, World!

モダンな世界のデファクトスタンダードな亜種:

#include <iostream>
int main() { std::cout << u8"Hello, World!"; }
Hello, World!

むかしはこれがモダンだった亜種:

#include <iostream>
int main() { std::wcout << L"Hello, World!"; }
Hello, World!

悲しみの亜種:

#include <iostream>
// Note: C++17 Deprecated
#include <codecvt>
#include <locale>
int main()
{
  std::cout
    <<  std::wstring_convert
        < std::codecvt_utf8_utf16
          < char16_t
          >
        , char16_t
        >().to_bytes( u"Hello, World!🍣" )
    ;
}
Hello, World!

悲しみの亜種の亜種:

#include <iostream>
// Note: C++17 Deprecated
#include <codecvt>
#include <locale>
int main()
{
  std::cout
    <<  std::wstring_convert
        < std::codecvt_utf8
          < char32_t
          >
        , char32_t
        >().to_bytes( U"Hello, World!🍣" )
    ;
}
Hello, World!

ひつようじゅうぶん:

#include <cstdio>
int main() { puts( "Hello, World!" ); }
Hello, World!

そういうの、こせいじゃないから…:

#include <cstdio>
int main() { printf( "%s, %s!", "Hello", "World" ); }
Hello, World!

さいしゅうへいき:

#include <cstdlib>
int main() { system( "echo Hello, World!" ); }
Hello, World!

ばいなりあん:

#include <cstdio>
#include <initializer_list>
int main()
{
  for
  ( auto b 
  : { 0x48, 0x65, 0x6c, 0x6f
    , 0x2c, 0x20, 0x57, 0x6f
    , 0x72, 0x6c, 0x64, 0x21
    }
  )
    putchar( b );
}
Hello, World!

2. るなてぃっく 編

Note: "なんいど" とは別のいみで "るなてぃっく" ・x・

ふてくされ:

#include <cassert>
int main() { assert( false && "Hello, World!" ); }
prog.exe: prog.cc:2: int main(): Assertion `false && "Hello, World!"' failed.
Aborted

こんぱいるたいむ・ふてくされ:

int main() { static_assert( false, "Hello, World!" ); }
prog.cc: In function 'int main()':
prog.cc:1:29: error: static assertion failed: Hello, World!
 int main() { static_assert( false, "Hello, World!" ); }
                             ^~~~~

がめんにでれば、いいんでしょ:

#include <Hello, World!>
int main() { }
prog.cc:1:10: fatal error: Hello, World!: No such file or directory
 #include <Hello, World!>
          ^~~~~~~~~~~~~~~

がめんにでれば、いいんでしょの亜種:

#pragma Hello, World!
int main() { }
prog.cc:1: warning: ignoring #pragma Hello  [-Wunknown-pragmas]
 #pragma Hello, World!

がめんにでるとは・・・:

"Hello, World!"
prog.cc:1:1: error: expected unqualified-id before string constant
 "Hello, World!"
 ^~~~~~~~~~~~~~~

えらーじゃないからはずかしくないもん:

"Hello, World!"
clang++ -E a.cpp
# 1 "a.cpp"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 325 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "<a.cpp>" 2
Hello, World!

おしまい・x・
ゆるゆると肩の力が抜けて頂けていれば幸いです。

・・・それから・・・
メリークリスマス!😃
🍣🍗🍰

UE4: 点や線を簡易的に3D空間内へ描画する方法、あるいは UKismetSystemLibrary::DrawDebug 系の紹介

概要

見栄えはともかく、3D空間内の任意の座標へ、点や線、あるいは立方体や球などを、さっくりと簡単に表示したい事がしばしばあります。特にデバッグ用途でそのような表示機能を上手く仕込んで置くと開発効率が向上します。

そのような場合に、わざわざ Billboard にテクスチャーを貼ったオブジェクトを用意したり、BOXを引き伸ばして配置したり、 UProceduralMeshComponent を使うまでもなく便利に使える UE4 に標準搭載された UKismetSystemLibrary::DrawDebug 系の機能を紹介したい。

f:id:USAGI-WRP:20171221045628p:plain

UKismetSystemLibrary::DrawDebug 系

UE4 に標準搭載された UKismetSystemLibrary::DrawDebugXxxx 系の関数群を用いると次のジオメトリー群の描画を簡単に実装できる:

  • header: #include "Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h"
  • Xxxx: ここは実際には Point などジオメトリーの種類で置換する。
Xxxx 効果
Arrow 矢印
Box 立方体
Camera カメラ
Capsule カプセル
Circle
Cone 円錐(長さで開きを定義)
ConeInDegrees 円錐(角度で開きを定義)
CoordinateSystem 座標系
Cylinder 円柱
FloatHistoryLocation 軌跡
FloatHistoryTransform 変形跡
Frustum 錐台
Line 線分
Plane 平面
Point 点(正方形)
Shpere
String 文字列

使い方

#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
#include "Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h"

// 適当なアクターに実装
void AMyPawn::Tick( float DeltaTime )
{
  Super::Tick( DeltaTime );

  // 2.5 秒ごとに表示を on/off する細工
  static float tt = 0;
  tt = FMath::Fmod( tt + DeltaTime, 5.0f );
  if ( tt < 2.5f )
  {
    // アクターの位置に赤い球を描画
    UKismetSystemLibrary::DrawDebugSphere( GetWorld(), GetActorLocation(), 100.0f, 12, FLinearColor::Red, 0.0f, 3.0f );
    // プレイヤーの位置を取得(本題と直接は関係しない)
    auto l0 = UGameplayStatics::GetPlayerPawn( GetWorld(), 0 )->GetActorLocation();
    // プレイヤーの位置からこのアクターの位置へ黄色の矢印を描画
    UKismetSystemLibrary::DrawDebugArrow( GetWorld(), l0, GetActorLocation(), 100.0f, FLinearColor::Yellow, 0, 3.0f );
  }
}

// プレイヤーのアクターに実装
void TestCharacter::Tick( float DeltaTime )
{
  Super::Tick( DeltaTime );

  // プレイヤーの位置に "PLAYER[0]" を緑色で描画
  UKismetSystemLibrary::DrawDebugString( GetWorld(), GetActorLocation(), TEXT( "PLAYER[0]" ), nullptr, FLinearColor::Green, 0 );
}

このシリーズの関数の引数は始めに UWorld を取り、続く前半はそれぞれ点であったり、始点と終点であったり、さまざま、後半は色と表示の持続時間、それと設定可能な場合は線の太さなどを与える仕組みです。具体的には API Reference をどうぞ。

この方法の特徴

  1. 見栄えはともかく、表示したいだけならばとても簡単。すぐに実装できる。
  2. 表示だけ、なので当たり判定を取りたい場合などには役に立たない。
  3. 色に不透明度を定義しても反映されない。
  4. 球などの立体も稜線しか描画できない。
  5. ライティングも適用されないし、発光もできない。

参考

UE4: TPSカメラの当たり判定を on/off する簡単な方法、あるいは USpringArmComponent のそれについて

概要

TPS テンプレートで生成されるような、プレイヤーの回りにぐるぐるとカメラを回す実装ではしばしば床、天井、柱、地形、キャラクター、その他の何らかの物体に対して「カメラの当たり判定」が有って欲しい状況と無い方が好ましい状況、あるいは実行中のその切り替えが欲しい場合がある。

UE4では一般にカメラとの当たり判定処理についてググると「カメラに当たりたくないオブジェクトのコリジョンからカメラを外せ」的な方法ばかり出てきますが、 TPS テンプレートで標準でプレイヤーに付いているようなカメラの場合はもっと簡単に対応できるので紹介したい。

前提知識: TPS のカメラとは何か

TPS テンプレートで標準で設定されるカメラは「プレイヤーキャラクターにアタッチしたスプリングアームにアタッチしたカメラ」です。

AMyCharacter <-- USpringArmComponent <-- UCameraComponent

TPS カメラの当たり判定の on / off

// TPS テンプレートで作られるプレイヤーキャラクターまたは同様の何かの ctor で
// TPS カメラの当たり判定を無効にする場合
AMyCharacter::AMyCharacter()
{
  // 生成とアタッチと回転制御はテンプレートまま(本題ではない)
  CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
  CameraBoom->SetupAttachment(RootComponent);
  CameraBoom->bUsePawnControlRotation = true;
  // これだけでカメラの当たり判定が無効になる
  CameraBoom->bDoCollisionTest = false;
}

// おまけ: ToggleCameraCollision 関数として実装する場合
void AMyCharacter::ToggleCameraCollision()
{ CameraBoom->bDoCollisionTest = ! CameraBoom->bDoCollisionTest; }

Note

  • 大雑把に on / off できれば構わない場合はおそらく最も簡単
  • オブジェクト個別やトレースチャンネルレベルでの細かい設定ではないので、その必要がある際は諦めてちまちまコリジョン設定する

Reference

  1. USpringArmComponent | Unreal Engine

UE4: UnrealWebServer Plugin が 1.4+ のバージョンアップで URL ハンドラーの path ワイルドカードと接続情報からの path の取得に対応

概要

プラグインのバージョン情報の定義は 1.4 のままですが、 UnrealWebServer が更新され、新機能が追加されました。今回追加された新機能は以前にバグレポートとパッチをなかのひとへ送った際に「ついでに将来的にこんな機能にも対応してくれたら嬉しいな😃」と相談していたもので、私の用途ではこれまでのアップデートも含め、 UnrealWebServer の実用性が必要十分に達しました。

isaratech.com

今回追加された機能

  1. Add URIHandler のパラメーター Path* によるワイルドカードを設定可能になった。
  2. Connection に対するメソッド Get Uri Path が追加されリクエストされたパスを接続情報から取得可能になった。

f:id:USAGI-WRP:20171212200248p:plain

f:id:USAGI-WRP:20171212200357p:plain

それでどんな事ができるようになったのか

今回の「URLハンドラーのパスのワイルドカード対応」と「接続情報からのパス取得」、それと以前の更新で追加された「リクエストのRAWボディー取得」と「レスポンスへのRAW書き出し」を組み合わせると、ファイルサーバーをエミュレートする事もできます。例えば、 JSON-RPC-2.0 で API を提供するアプリでもファイル単位のバイナリーを受け取ったり、提供したり、そんなような事を簡単に実装したいニーズがあるときに、 UWS を使えば少々のコストで簡単にそんなような API サブシステムを実装できるようになりました。

DELETE などもハンドラーとしては扱えるので、 いわゆる REST API 的な実装もかんたんに対応できるようになりました。

Related

usagi.hatenablog.jp

usagi.hatenablog.jp

CPP-QUIZ Q5 のこたえ、マクローリン展開による sin/cos の高速化に潜んでいた大きな誤差を生じるバグ

概要

この記事を書くきっかけは先日書いた記事 CPP-QUIZ from Unreal 2017 ( part-1/2; 出題編 ) の Q5. について、こたえとして掲載したコードについて、 Twitter にて @nekketsuuu さんから次のようなご指摘を頂いた事でした。ありがとうございます😃

どう見ても誤差が大きすぎるし(この検証の元となった出題では std::sin との誤差が最大でも 1.0e-6f に収まるようにしたいというものだった)、明らかに癖の強い周期性が見える。

ここ数日、お仕事などでまとまったプライベート時間をなかなか確保できず、数日お待たせしてしまいましたが、この問題の原因を調査し整理できる時間を確保できたので記事として整理する事にしました。

問題を確認する

f:id:USAGI-WRP:20171212032210p:plain

「にゃーん🐾」

とりあえず、現状の問題、誤差の発生状況を把握するため、[0 .. 2π] について視覚的にプロットを把握するに十二分と思われる適当な分解能で、

  1. std::sin を計算し、
  2. SinCosB を計算し、 sin 成分を取り出し、
  3. std::sinSinCosB の sin 成分の差の絶対値を計算して縦軸へ、入力の角度値を横軸へプロット

した絵です。こうしてみると、

  1. π/2 ごとに周期的に誤差が増減し、
  2. 最大誤差が 1.0e-6f どころか 1.0e-4f を超えてしまっている

ことが明らかです。

問題のコード(=「こたえ編で紹介した現行の UE4 実装と等価なコード」)

#define PI 3.14159265358979f
std::tuple< float, float > SinCosB( const float angle_in_radians )
{
  float quotient = angle_in_radians * 0.5f / PI;
  if ( angle_in_radians >= 0.0f )
    quotient = (float)( (int)( quotient + 0.5f ) );
  else
    quotient = (float)( (int)( quotient - 0.5f ) );
  float a = angle_in_radians - quotient * 2.0 * PI;
  float s = 0.0f;
  if ( a > 0.5f * PI )
  {
    a = PI - a;
    s = -1.0f;
  }
  else if ( a < -0.5f * PI )
  {
    a = -PI - a;
    s = -1.0f;
  }
  else
    s = +1.0f;
  float p = a * a;
  // [ 1 / 2!, 1 / 3!, 1 / 4!, .., 1 / 11! ]
  constexpr float f2 = 1.0f / 2.0f;
  constexpr float f3 = f2 / 3.0f;
  constexpr float f4 = f3 / 4.0f;
  constexpr float f5 = f4 / 5.0f;
  constexpr float f6 = f5 / 6.0f;
  constexpr float f7 = f6 / 7.0f;
  constexpr float f8 = f7 / 8.0f;
  constexpr float f9 = f8 / 9.0f;
  constexpr float f10 = f9 / 10.0f;
  constexpr float f11 = f10 / 11.0f;
  return std::make_tuple
  ( a * ( +1.0f + p * ( -f3 + p * ( +f5 + p * ( -f7 + p * ( +f9 * p * ( -f11 ) ) ) ) ) )
  , s * ( 1.0f + p * ( -f2 + p * ( +f4 + p * ( -f6 + p * ( +f8 + p * ( -f10  ) ) ) ) ) )
  );
}

誤差が 1.0e-6f 未満となる十分な精度が得られるまでマクローリン展開を計算しています。この実装は std::sinstd::cos を使った同様の計算よりも wandbox 環境では 1.45 倍高速に動作した、めでたしめでたし、というものでした。

ところが、どうやらこの SinCos は一見すると正しくマクローリン展開を計算しているようで、残念なバグが潜んだコードだったのです。

ナ ナンダッテー!!
Ω ΩΩ

問題について調査する

一般的に浮動小数点数の取り扱いで生じる幾つかの可能性が思いつきましたが、これと直感で断言できるほどの確信は無く、簡単なものから可能性を排除していく事にしました。

1. 単純に浮動小数点数型の精度を上げてみる

float 固定だった浮動小数点数の型を template 化して同様にプロットを眺めてみます:

template < typename T > constexpr auto pi = (T)3.14159265358979323846264338327950288;

template < typename T >
std::tuple< T, T > SinCosC( const T angle_in_radians )
{
  T quotient = angle_in_radians * (T)0.5 / pi< T >;
  if ( angle_in_radians >= (T)0 )
    quotient = (T)( (int)( quotient + (T)0.5 ) );
  else
    quotient = (T)( (int)( quotient - (T)0.5 ) );
  T a = angle_in_radians - quotient * (T)2.0 * pi< T >;
  T s = (T)0;
  if ( a > (T)0.5 * pi< T > )
  {
    a = pi< T > - a;
    s = (T)-1;
  }
  else if ( a < (T)-0.5 * pi< T > )
  {
    a = -pi< T > - a;
    s = (T)-1;
  }
  else
    s = (T)+1;
  T p = a * a;

  // [ 1 / 2!, 1 / 3!, 1 / 4!, .., 1 / 11! ]
  constexpr T f2 = (T)1 / (T)2;
  constexpr T f3 = f2 / (T)3;
  constexpr T f4 = f3 / (T)4;
  constexpr T f5 = f4 / (T)5;
  constexpr T f6 = f5 / (T)6;
  constexpr T f7 = f6 / (T)7;
  constexpr T f8 = f7 / (T)8;
  constexpr T f9 = f8 / (T)9;
  constexpr T f10 = f9 / (T)10;
  constexpr T f11 = f10 / (T)11;
  
  return std::make_tuple
  ( a * ( (T)1 + p * ( -f3 + p * ( +f5 + p * ( -f7 + p * ( +f9 * p * ( -f11 ) ) ) ) ) )
  , s * ( (T)1 + p * ( -f2 + p * ( +f4 + p * ( -f6 + p * ( +f8 + p * ( -f10 ) ) ) ) ) )
  );
}

f:id:USAGI-WRP:20171212034734p:plain

「( ゚∀゚)o彡゚おっぱい!おっぱい!」

SinCosC の template の実引数に float (プロット: 青), double (プロット: 橙), long double (プロット: 灰) を与え、それぞれの浮動小数点数型の精度ごとに std::sin との差の絶対値を絵にしてみましたが、この結果からは「どうやら問題は浮動小数点数型の精度に少なくとも直接的には起因した問題ではない」という事が明瞭となりました。

ついでの蛇足調査(1/2); そもそも float vs. long double の値の誤差ってどれくらいでるものなの?

[0 .. 2π] について図示に必要十二分な分解能で floatdouble それぞれの値の差の絶対値がどれくらい生じるかの図も用意してみました。

f:id:USAGI-WRP:20171212040439p:plain

・・・こんなもんです・x・

ついでの蛇足調査(2/2); SinCos の前半部分が無いとどうなるのか?

SinCos の実装は大きくわけて2段階の工程となります。

  1. [-π .. +π] に射影しつつ 2π * quotient + reminder を経由して、最終的に -π/2 .. +π と cos 用の符号を保存
  2. マクローリン展開に基いて必要十分な精度まで正弦と余弦を計算する

この前半部分をやらずに、直接後半のマクローリン展開へ入力された値を放り込むとどんな事が起こるのか、もののついでにグラフを作っておきました。

f:id:USAGI-WRP:20171212035803p:plain

みょーん・・・と、なってしまい どころか πあたりで既に実用性がかなり怪しい精度になっています。この問題を隠蔽するために最も誤差が小さく他の象限とは対称となるだけの [-π/2 .. π/2] に角度を落としてマクローリン展開部分を計算していたわけです。

そもそもマクローリン展開ってそんな誤差がでるものなの?

でません。少なくとも数学的には。そんなわけで、疑う先はマクローリン展開の実装コードではないかと絞り込めてきました。

そもそも、入力値が π/2 の場合、数学的に正確な正弦は 1 ちょうどです。しかし、どうやら SinCosB やそれを template 化した SinCosC の実装では、入力値が π/2 のときに誤差が最大値となっている事がわかっています。なので、初期値が 0 付近の値の計算や 0 付近の値に対する誤差の計算の信頼性が云々・・・という事でも無さそうです。

ちなみに、入力値が π/2 の場合の場合の SinCosB または SinCosC の出力値は float, double, long double の何れで計算した場合も、出力値は 0.999843 となり、 vs. std::sin の差の絶対値は 0.000157 となります。

f:id:USAGI-WRP:20171212041927p:plain

これは浮動小数点数型の精度、マクローリン展開の精度の何れからも不可解な結果です。

そこで、実装の分かり易く愚直なマクローリン展開の正弦を計算するコードを書いて試してみます。

#include <iostream>
#include <iomanip>

template < typename T > constexpr auto pi = (T)3.14159265358979323846264338327950288;

/// @brief オーバーフローしない限りの任意の階数でマクローリン展開による正弦を計算
/// @tparam N マクローリン展開する階数
/// @tparam T 入力および計算と出力に用いる浮動小数点数型
/// @param a 入力角度 [rad]
/// @return 正弦値 [-]
template < std::uint8_t N, typename T >
auto sin( const T a )
{
  static_assert( N % 2 == 1 );
  static_assert( N <= 19, "factorial be overflow" );
  
  // 展開された級数を加算/減算し結果を得る変数
  T result = a;
  // 符号は項ごとに反転する
  bool is_positive = false;
  // マクローリン展開を項ごと愚直に計算
  for ( decltype( N ) n = 3; n <= N; n += 2, is_positive = ! is_positive )
  {
    // 項の分数の分子の側となる指数を逐次愚直に乗算して結果を得る変数
    T             power     = 1;
    // 項の分数の分母の側となる階乗を逐次愚直に乗算して結果を得る変数
    std::uint64_t factorial = 1;
    // 項の分子と分母それぞれを愚直に乗算を繰り返して計算
    for ( auto m = n; m; --m )
    {
      power     *= a;
      factorial *= m;
    }
    // 項の符号に基いて計算中の結果へ加算/減算し計算精度を高める
    if ( is_positive )
      result += power / (T)factorial;
    else
      result -= power / (T)factorial;
  }
  // マクローリン展開を要求された階数だけ計算した結果を返す
  return result;
}

やや丁寧すぎるくらいにコメントをソースへ入れたので文章での解説は省略します。この sinfloat, double, long doubleπ/2 を入力して出力を眺めてみましょう。

int main()
{
  constexpr auto n = 11;
  std::cout 
    << std::fixed << std::setprecision( 32 ) << sin< n >( pi< float       > / 2 ) << '\n'
    << std::fixed << std::setprecision( 32 ) << sin< n >( pi< double      > / 2 ) << '\n'
    << std::fixed << std::setprecision( 32 ) << sin< n >( pi< long double > / 2 ) << '\n'
    ;
}

n = 11 (問題のある SinCosB などと同じマクローリン展開の階数)の結果:

0.99999988079071044921875000000000
0.99999994374105094507854119001422
0.99999994374105087037701150576297

あっさり、すっきり、精度6桁確保できていますね・x・

ちなみに、 n = 13 にすると、

0.99999994039535522460937500000000
1.00000000066278027510691117640818
1.00000000066278009003360033313257

さらに桁単位で誤差が縮みます。

つまり、 SinCosBマクローリン展開の実装に問題がある可能性がとても濃厚に疑われます🐰

SinCosB の何が問題だったのか?

次の2つのコードは同じでしょうか、また、違いがあるとすればどちらが正しいマクローリン展開でしょう。

// SinCosB のマクローリン展開の最終的な計算の実装
auto s0 = a * ( 1.0f + p * ( -f3 + p * ( +f5 + p * ( -f7 + p * ( +f9 * p * ( -f11 ) ) ) ) ) );
// クイズ出題にあたり参考とした元の UE4 FMath::SinCos での実装
auto s1 = ( ( ( ( ( ( -f11 ) * p + f9 ) * p - f7 ) * p + f5 ) * p - f3 ) * p + 1.0f ) * a;
s0: 0.99984318017959594726562500000000
s1: 0.99999988079071044921875000000000

CPP-QUIZ from Unreal 2017 では、出題の元ネタとして UE4 の FMath ライブラリーの実装に基きつつ、 UE4 についてはまったく知らない方でもほぼ純粋に C++ における実装問題として楽しめるよう、またコードも私なりに読みやすいよう、 "工夫" したつもりでした。一般に、数式でもマクローリン展開は低い階数から項を並べて書くことが多いので、ややこしい記述となる実装部位について読者にも少しでも読みやすいようにと思い、参考とした UE4 FMath ライブラリーの実装では右側が低い階数となっていた実装を「これは問題ない、同じ動作となる "はず" 」と書き換えていたのです。

よーく、よーく、眺めると、 s1 では +f9 と次の項との計算はもちろん + 演算子となっている ところが、 s0 では * 演算子となっています。 s0s1 を比較しやすいよう、 s1 の記述を s0 のように、つまりマクローリン展開についてよく数式で表す際に用いられるように低い階数から並べると、次のようになります。

                                                                     |--- Mistake!
auto s0 = a * ( 1.0f + p * ( -f3 + p * ( +f5 + p * ( -f7 + p * ( +f9 * p * ( -f11 ) ) ) ) ) );
auto s1 = a * ( 1.0f + p * ( -f3 + p * ( +f5 + p * ( -f7 + p * ( +f9 + p * ( -f11 ) ) ) ) ) );
                                                                     |--- CORRECT!

やれやれ・・・なんという結末の「なあんだ、そんなこと!」感。調査に昨晩少々と今朝、記事にしながら併せて90分くらい掛けてしまいました。コードの記述がややこしい実装をやむを得ずする場合には、こうしたごく単純な書き間違えによるバグを埋め込んでしまう可能性が高まります。今回の例はやむを得ず高速化したいという要求から単純ではない実装を試みる必要がありましたが、気持ちが緩み過ぎて、そうしたバグが埋め込まれてしまいやすい実装を行う上で十二分に必要な措置を講じ確実で正確な検証を行うべきところを怠った事が実質的な "バグ" だったと言えます。

修正版 SinCosB の正弦と std::sin の差の絶対値:

f:id:USAGI-WRP:20171212071323p:plain

最大でも 1.0e-6 未満に収まるようになりました😃 より具体的には入力値が 1.51163 [rad] ( ≃ 86.61 [deg] )などで最大の誤差 1.79e-7 が観測されます。

幸い、今回は実害の無いクイズで、なおかつ異常に気づいた @nekketsuuu さんからの指摘を基に、早々にバグを発見、修正する記事をこのように書けましたが、これが実際の製品に埋め込まれていた可能性や、保守担当者が私ではなかった場合(特に若い子にこれを対処させる必要性が生じた場合など)を考えると、なかなか、危ないところでした。反省しつつ、もっとコードを、そしてメタ的(≃試験やアサーション)に安全に、検証を怠らずに、さらにメタメタ的な素養(≃意識、習慣)を強化したいと思います。

それでは、この後日談ネタも含めて CPP-QUIZ from Unreal 2017 の "楽しさ" として頂ければ幸いです。🎅メリクリ🎄

CPU: Intel が SIMD 命令の拡張にアフィン変換を追加する、という話の中身と影響する分野について

IntelSIMD 命令の拡張にアフィン変換を追加するらしい(参考2) との事で、 "Intel® Architecture Instruction Set Extensions and Future Features Programming Reference" (参考1) を見てきました。アフィン変換は Computer Graphics 界隈の人々が反応しやすいキーワードですが、今回の話はあまり "一般的な狭義の意味での CG 界隈" とは関係無いよ(いまのところ)という話です。

追加される(予定の) アフィン変換系の命令群

  1. GF2P8AFFINEINVQB ; Galois Field Affine Transformation Inverse (=ガロア域アフィン逆変換)
  2. GF2P8AFFINEQB ; Galois Field Affine Transformation (=ガロア域アフィン変換)

命令の概要

何れもガロア域 28 におけるアフィン変換の計算を SIMD レジスターを用いて行うCPU命令。 Inverse の方は 8x8 bit の行列 A 、 8-Bit ベクターx, b について A * inv( x ) + b を計算、他方は A * x + b を計算するもの。

この計算は AES 暗号の S-box 回りの計算。もし暗号技術に興味があり、それが一体何なのかについてより詳しく理解したい方には書籍「暗号技術のすべて」(参考6)のAES暗号の解説がたいへん詳しくわかりやすいのでおすすめ。

想定される主な用途

  • AES 暗号技術( AES 暗号の "Rijndeal S-box" まわりの計算が今回話題の GF2P8AFFINE 系命令の適用そのもの; 参考3,4,5 )
    • Note: AES = Advanced Encryption Standard (参考5)
    • Note: Rijndeal (ラインダール; 参考3)

(一般的な狭義の意味での)CG屋さんへの影響

と、いうわけで、「アフィン変換」と聞いて反応しそうな(2次元、3次元での線形幾何を主戦場とする)CG 屋さんたちには今回は関係無いからひとまず安心して寝ましょう、というお話でした。ちなみに、 AVX のレジスターは 512-Bit なので、ここでいう狭義の CG 界隈で一般的な 32-Bit float の 4x4 行列を扱おうにもオペランドを1つロードしただけでレジスターが埋まってしまいます。 GPU には 16-Bit float の対応もありますが、それはそれで CPU はネイティブに対応していませんし、 IEEE754/binary16 相当では 10 進数で 3 桁程度の精度しかありませんから実世界での用途は限られます。 AVX2048 …みたいなものが来れば CPU で 32-Bit float のここでいう狭義の CG 屋さん向けのアフィン変換命令も乗る事もあるのかもしれませんが、たぶん、少なくとも数年単位の近い未来までの事としては、"それは GPU でやってくれ" と Intel も考えているような気がします(関係者じゃないので知らんけど😋)。

参考