空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

Firefox設定メモ(2018年1月版)

Firefox設定メモ ~MS PゴシックをMeiryo UIに置換~ - 空想犬猫記 を久々にアップデート。

Firefox Quantum では使えなくなる可能性はあるものの、現状の Firefox 57 では、まだ userContent.css によるフォントの置換テクニックが有効であった。(参考: Modifying the Default Skin - Mozilla | MDN

最新の設定では、置換先のフォント名は明示的には書かず、すべて sans-serif にしておき、一方で sans-serif のフォント設定を、自分が一番読みやすいフォントに設定しておくという方式にした。個人的には Yu Gothic UI がお気に入り。

@charset "UTF-8";
@font-face {
  font-family: "MS Pゴシック";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS PGothic";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS P明朝";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS PMincho";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS UI Gothic";
  src: local("sans-serif");
}

Rust雑感

年初から、また少しだけ Rust(The Rust Programming Language)というプログラミング言語を覚え直していた。リンク先のサイトには以下のような特徴が挙げられている。

  • zero-cost abstractions
  • move semantics
  • guaranteed memory safety
  • threads without data races
  • trait-based generics
  • pattern matching
  • type inference
  • minimal runtime
  • efficient C bindings

平たく言うと、メモリ安全で、データ競合も起こらず、C言語並みに軽くて速いという。さらに調べてみると、ジェネリクスはもちろん、trait objects を使った仮想関数テーブルによるポリモーフィズムに加えて、メンバ変数を持てる enum と match 文でパワータイプによるポリモーフィズムもサポートされているという尖りまくりの言語であることがわかった。

しかしながら、

trait objects は、なんかちょっと欲張って取ってつけた感が否めない。
途中から出てきた ref キーワードも、なんか必然性を感じない。
ところどころでシンタックスが美しくない(例:多次元配列の定義 [[u8;9];9])

このように現時点では C/C++ に取って代わりうる言語の中で、頭一つ抜き出ているものの、少し不安な要素もある。VSCodeプラグインや、開発支援ツールの充実度でも、某G言語にはまだまだ遠く及ばない感じ。Go言語のような巧みなシンタックスのデザインと Rust の厳密で突き抜けた部分を合わせると最強のシステム記述言語になり得るのではないだろうか。現状はまだまだ人柱を絶賛募集中といったところか。

return throw 問題

今日ハマったC++の落とし穴。次のコードの出力結果を予想してください。

#include <iostream>
#include <exception>

using namespace std;

static void assertZero(int i)
{
  if (i == 0)
    return

  //
 // I'm sure it is not a zero!!
  //
  throw exception("Not a zero");
}

int main()
{
  try
  {
    assertZero(0);
    cout << "0 is zero.\n";
  }
  catch (...)
  {
    cerr << "0 is not zero.\n";
  }

  try
  {
    assertZero(1);
    cout << "1 is zero.\n";
  }
  catch (...)
  {
    cerr << "1 is not zero.\n";
  }

  return 0;
}

これは

0 is zero.
1 is not zero.

となって欲しいところだけど、実際には以下のようになる。

0 is not zero.
1 is zero.

これ見て紅茶を噴きかけたのだが、こうなった原因は、assertZero 関数の return 直後にセミコロンをつけ忘れたことにある。C++の仕様によれば return の後には式を書くことができて、throw は void 型の式だったんですな。つまり返り値が void 型の関数において return throw exception("..."); と書くのは仕様上セーフであるらしい。結果として assertZero 関数は

static void assertZero(int i)
{
  if (i == 0)
    return throw exception("Not a zero");
}

と解釈され、めでたく全く意図とは正反対の挙動をするというわけでした。Go や TypeScript など、セミコロンの要らないモダンな言語に侵食されると、この手のミスが増えることが予想されます。これを機に中括弧を省略しない宗派に切り替えることも検討中。

(再現環境:Visual Studio 2017)

一時オブジェクトの寿命

先輩にバグじゃないの?と指摘されたコード。

class MyString
{
public:
  MyString();
  MyString(const std::string& s);
  :
};

extern const std::string& GetName(int idx);

int main()
{
  for (int i = 0; i < N; ++i)
  {
    const MyString& s = GetName(i);
    // この後、forのスコープ内で文字列 s を使うのは安全か?
  }
  
  return 0;
}

このコードは、以前、レガシーな std::string を使ったコードを国際化対応した文字列クラスに一括置換したときに生じたものだ。まだプロダクションには使われていないが、一瞬、嫌な汗をかいた。結論から言うとこれは合法だった。コンパイラも完全にスルーしていたので気付かず。

何が起こっているかというと、GetName() で std::string の参照が返った後、MyString の変換コンストラクタ MyString(const std::string&) が呼ばれ、一時オブジェクトが作られる。一時オブジェクトをconst参照で受けた場合、一時オブジェクトはconst参照に束縛されるというルールがあり、const参照変数のスコープまで延命される。したがってfor文のスコープ内で使うことができる。

まぁ、参照で受けてコピーを作らないという意図も叶っておらず、単にややこしいだけなので、早々に直したほうが良いだろう。

VC++のunordered_set/unordered_mapが遅い件

正確に言うと、insertが同種のset/mapに比べて圧倒的に遅い。

最近、メモリの節約のために辞書的なオブジェクトを作成して、同値のオブジェクト(例えば文字列)を再利用するようなコードを書いたところ、unordered_set/unordered_mapで思うような性能が得られなかった。

コンテナ以外のアロケーションの影響を受けないように、あらかじめ文字列を用意して、それを辞書に登録する以下のサンプルコードを書いて、Visual Studio 2015でプロファイルをとってみた。

#include "stdafx.h"

using namespace std;

namespace
{
  struct char_cmp : public binary_function<const char*, const char*, bool>
  {
    result_type operator()(first_argument_type l, second_argument_type r) const
    {
      if (!l)
      {
        if (!r) return false;
        return true;
      }
      else
      {
        if (!r) return true;
        return strcmp(l, r) < 0;
      }
    }
  };

  struct char_hash : public unary_function<const char*, size_t>
  {
    size_t operator()(argument_type key) const
    {
      return hash_value(key);
    }
  };

  struct char_equal_to
  {
    typedef const char* first_argument_type;
    typedef const char* second_argument_type;
    typedef bool result_type;

    bool operator()(first_argument_type pL, second_argument_type pR) const
    {
      return strcmp(pL, pR) == 0;
    }
  };

  template <typename Set> __declspec(noinline) void TestSet(const vector<string>& samples)
  {
    Set set;
    for (auto& s : samples) set.insert(s.c_str());
  }
}

int main()
{
  vector<string> samples;
  samples.reserve(100000);

  default_random_engine generator;
  uniform_int_distribution<int> distribution(33, 126);

  for (int i = 0; i < 100000; ++i)
  {
    samples.emplace_back();
    auto& str = samples.back();
    str.reserve(300);
    for (int j = 0; j < 300; ++j)
      str += distribution(generator);
  }

  TestSet<unordered_set<const char*, char_hash, char_equal_to>>(samples);
  TestSet<set<const char*, char_cmp>>(samples);
  return 0;
}

で、結果がこちら

f:id:xoinu:20161210085710p:plain

サンプルの要素数が増えれば増えるほど差は広がるばかり。確認のためTDM-GCCで同等のコードを書いてみたところ、むしろunordered_set/unordered_mapの方がinsertは高速だったので、これはVisual StudioSTLの実装特有の問題っぽい。

Visual Studio上でunordered_set/unordered_mapを旧来のset/mapの代わりに使う際には、必ずベンチマークをとって、挿入性能劣化に見合う検索性能の向上の恩恵に与れるかどうかを確認したほうが良さそうだ。

やれやれ……

追記:
xoinu.hatenablog.com
2年前にint型のコンテナで挿入のパフォーマンスのチェックをやってた。そのときの結果と少し矛盾している。これを見るにVC++のxhashヘッダにあったstd::hash_value()関数がよろしくないのかもという気がする。

再帰呼び出しをループに置き換える

半年に一度くらい、古いコードで使っている再帰呼び出しが原因でスタックオーバーフローになるという不具合が発見され、その都度ループで書き直す修正を入れている。再帰呼び出しをループに展開する方法は、「幅優先探索」や「深さ優先探索」というキーワードで探すと見つけることができる。そのエッセンスだけメモしておく。

再帰呼び出しのうち、トップダウンで処理していくものについては、キューを使うと比較的簡単に展開できる。たとえば

struct Node {
  Node* left;
  Node* right;
  :
};

という構造体があって、

void ProcessNode(Node* node)
{
  :
  : (do something)
  :

  if (node->left)
    ProcessNode(node->left);

  if (node->right)
    ProcessNode(node->right);
}

のような再帰呼び出しがあるとき、ProcessNode関数は以下のように展開できる。

void ProcessNode(Node* node)
{
  list<Node*> nodeList;
  nodeList.push_back(node);

  while (!nodeList.empty())
  {
    node = nodeList.front();
    nodeList.pop_front();

    :
    : (do something)
    :

    if (node->left)
      nodeList.push_back(node->left);

    if (node->right)
      nodeList.push_back(node->right);
  }
}

ボトムアップに処理していくものも基本的な考え方は一緒であるが、

  1. キューの代わりにスタックを使う
  2. スタックを積むときと実際に処理をするときの二度、ノードを訪れることになるので、今どっちのタイミングなのか、知るためのフラグが必要

になる点が少し面倒になる。模擬コードは以下のようになるはず(自分で試してね)。

namespace {
  struct Data {
    Data(Node* n) : node(n), isTraversed(false) {}
    Node* node;
    bool isTraversed;
  };
}

void ProcessNode(Node* node)
{
  vector<Data> nodeList;
  nodeList.push_back(node);

  while (!nodeList.empty())
  {
    auto& data = nodeList.back();
    node = data.node;

    if (!data.isTraversed)
    {
      data.isTraversed = true;

      if (node->left)
        nodeList.push_back(node->left);

      if (node->right)
        nodeList.push_back(node->right);
    }
    else
    {
      nodeList.pop_back();
      :
      : (do something)
      :
    }
  }
}

再帰呼び出しで書くと、単にコード中の "do something" の位置を if 文の前に持ってくるか後ろに置くかだけの差であり、記述としては簡便なうえに、マシンスタックを利用したほうがヒープよりも高速に動作する。つまりスタックオーバーフローを起こさない限りは再帰のほうが一概に優れているので、もろもろ考慮してどちらの実装方法を選ぶか決めましょう。