読者です 読者をやめる 読者になる 読者になる

ブログのしゅーくりーむ

技術的なメモとかライフログとか。

C++ のラムダ式のデフォルトキャプチャの対象となる変数について確認していた

C++初学者なので勉強のため会社の有志の人々と Effective Modern C++ の輪読会をやっているのですが、その際にラムダ式のデフォルトキャプチャの挙動に関する話題になり、個人的に無性に気になってしまったので調べてみました。 もし誤りや補足などございましたら、コメント等でご指摘いただければ幸いです。

ながすぎる; よんでない

疑問

デフォルトコピー・参照キャプチャ([=], [&])を記述した際のラムダ式のデータメンバとしては、ラムダ式を定義した外のスコープの変数すべてが対象になるのか、必要なメンバしか持たないのか、という点が気になりました。 例えば外のスコープで下記のようにクソデカイ容量の変数が存在したとして、必要であろうがなかろうが問答無用でコピーされうるのか・・・。

using namespace std;

int main() {
  vector<int> large_vec;

  ... // 紆余曲折あって large_vec のキャパシティがクソデカくなっていると想定する

  auto lambda = [=]{}; // lambda のメンバにはクソデカイ vector は、例え使ってなくても含まれるのか??

  return 0;
}

輪読会参加メンバーで打ち立てた予測としては、必要な変数のみコピーされるというものでした

回答

言語仕様上、必要な変数のみコピーされるようです。 (・・・但し後述の通り、実際にデバッガ当てて調べていった感じ、そうではない箇所もあり・・・)

調査結果

言語仕様を読んでみた

今回は The Standard : Standard C++ に掲載されている C++14 の仕様のドラフト N4296 を眺めてみました。

ラムダ式に関する記述がある 5.1.2 節の 12,13 番 に、今回扱う疑問点に関するっぽい記述を発見しました。曰く

デフォルトキャプチャを持つラムダ式は ... (中略) ... compound-statement(ラムダ式の {} の中の文のこと) が下記の条件である場合エンティティ(this と変数のこと) を暗黙的にキャプチャする。 エンティティが odr-used である、もしくは ... (略)

... (略) ラムダ式によってキャプチャされるエンティティは、ラムダ式を含むスコープで ord-used である ... (略)

odr-used な変数がキャプチャの対象になりそうです。 ・・・で、 odr-used というのは何だよという話になりますが、大雑把にいうと定義を必要とする変数の使用のことのようです。 つまるところ、上記ドラフトの記述から「定義を必要としない変数の使用しかされていない(not odr-used な)変数はデフォルトキャプチャを書いてもキャプチャされない」と言えそうです。

実際のコンパイル結果

実際どのようにキャプチャされるのか確認してみました。今回は下記のソースコードを手元の Mac OS X 10.11 にて下記の環境でコンパイルし、 lldb でラムダ式のキャプチャ結果を拾ってみます。

$ g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/c++/4.2.1
Apple LLVM version 7.3.0 (clang-703.0.31)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

デフォルトコピーキャプチャ以外の振る舞いもついでに見てみたいので、幾つかのキャプチャとラムダ式内の文のパターンを用意してみました。

using namespace std;

int main() {
  vector<int> unused_data = {0};
  vector<int> data = {0};

  auto cpcap_ex = [data]{};
  auto cpcap = [=]{ cout << data.size() << endl; };
  auto cpcap_emptyimpl = [=]{};

  auto refcap = [&]{ cout << data.size() << endl; };
  auto refcap_ex = [&data]{};
  auto refcap_emptyimpl = [&]{};

  return 0;
}

デバッガで動作確認したい都合、今回は -g を付けてコンパイルします。

$ g++ -g -std=c++0x test.cpp

コンパイル後に lldb で return あたりの行でブレークポイントを貼って各変数の内容を見てみます。

$ lldb a.out
...
(lldb) b test.cpp:<行数>
...
(lldb) r
...
(lldb) p cpcap_ex
((anonymous class)) $0 = {
  data = size=1 {
    [0] = 0
  }
}
(lldb) p cpcap
((anonymous class)) $1 = {
  data = size=1 {
    [0] = 0
  }
}
(lldb) p cpcap_emptyimpl
((anonymous class)) $2 = {}
(lldb) p refcap
((anonymous class)) $3 = {
  data = size=1
}
(lldb) p refcap_ex
((anonymous class)) $4 = {
  data = size=1
}
(lldb) p refcap_emptyimpl
((anonymous class)) $5 = {}

表題とは外れてしまいますが、まずデフォルトキャプチャではなく明示的にキャプチャした場合は、 odr-used とか関係なくキャプチャされているようですね。 また、デフォルトキャプチャかつラムダ式の文内で data.size() みたいな感じで odr-used すると、対象の変数はキャプチャされているようです。 更にデフォルトキャプチャでラムダ式の文を空にした場合は、何もキャプチャされないという結果になりました。

ちなみに下記のようにラムダ式の文内に odr-used な式を書いてみたのですが・・・

class undef *up;
auto cpcap_undef = [=]{ up; }; // {} 内の式は discarded-value expression であり、 up は odr-used

この場合はキャプチャされているっぽく見えます。自分の仕様の解釈が誤っているのか、あるいは何か読み落としているのか・・・教えてえろいひと!

(lldb) p cpcap_undef
((anonymous class)) $0 = (up = 0x0000000000000000)

ts_mruby + mruby-mrmagick で画像リサイズするリバースプロキシをサクッと実装してみた

今日は夏コミのようですね!僕は今年は特に原稿を描いておらず用事も無いためコミケに縁のない夏を過ごしてしまいました。

ところで最近拙作の ATS(Apache Traffic Server) プラグインである ts_mruby にレスポンスボディをいじくるメソッドを追加し、ふと思い立って画像リサイズのロジックを mruby で書いてみました。

そのレスポンスボディ加工メソッドは ATS::Filter#transform! になります。 このメソッドではブロックまたは Proc オブジェクトを渡すとブロックパラメータに加工前のレスポンスボディが渡され、評価値でレスポンスボディを差し替えます。

f = ATS::Filter.new
f.transform! do |body|
  # append text to the end of response body
  body + "rewritted by ts_mruby :D"
end

ちなみに ngx_mrubyNginx::Filter#body 相当のメソッドは実装できていません。また ATS::Filter#transform! ではブロックの外の変数へのアクセスを許可していません(ブロックの内外で実行時の context を共有していないため)。これらは現状、 ts_mruby を実装する上でフックの挿入をシンプルにして、 mruby スクリプトを書く側に意識させない作りにしているために生じている制限になっています。。。 将来的には 任意のフック時点に挿入可能にして Nginx::Filter#body 相当のメソッドを実装する・・・かもしれません。

話が逸れましたが、 ATS::Filter#transform! を使えばボディの加工はできます。後は画像ファイル取得時のレスポンスボディを ImageMagick でイイカンジに加工してみます。 今回は ImageMagick の mruby バインディング mruby-mrmagick を使ってみます。 要件としては下記を念頭に置きました。

  • ファイルへの読み書きを行わない
    • ATS の主要用途はプロキシだしワーカスレッドに極力 I/O させず処理したい
  • リクエストのクエリ文字列でリサイズの挙動を変更可能にする
  • ATS::Filter#transform! の制約をなんとか突破する

これらの要件を満たす mruby スクリプトを、下記の工夫を織り込んで書いてみました。(もろもろのファイルなどはこちら

  • ImageMagick の blob でボディを加工・取得
  • クエリ文字列を ATS::Request#args を使って取得
  • ATS::Filter#transform! には必要なパラメータを予め Proc#curry で部分適用しておいた Proc オブジェクトを渡す
# parse parameters
params = {}
req = ATS::Request.new
req.args.split('&').each do |arg|
  key, value = arg.split('=')
  params[key] = value
end

# set resizing logic
resizer = nil
if params.has_key?('scale')
  resizer = Proc.new do |scale, body|
    img = Mrmagick::ImageList.new
    new_img = img.scale(scale)
    new_img.from_blob(body)
    new_img.to_blob
  end.curry.(params['scale'].to_f)
end

# set filter if params is valid
if (resizer)
  filter = ATS::Filter.new
  filter.transform! resizer
else
  ATS::rputs 'Parameters are invalid ...'
  ATS::return ATS::HTTP_BAD_REQUEST
end

mruby-mrmagick でラクをしているとは言え、割とシンプルに記述できた気がします!

ちなみに ATS のキャッシュが有効 な場合、 ATS::Filter#transform! で加工した結果がキャッシュに入ります。 これを活用すれば、このリサイズのロジックもキャッシュヒットしている間は都度走らせずに済み、 CPU リソースの削減が可能かと思います。 ・・・ただし、そのためには上記スクリプトにキャッシュヒットしたかどうかのロジックも入れる必要があります(上記ではそれが無いため、キャッシュヒットすると段々とサイズが大きくor小さくなってしまう場合が!)

週報仲間というやつを試してみる

プライベートにおける作業、たとえば英語の勉強だとかちょっとしたプロダクトを実装してみるとか、はたまたプラモデルを組み立てるだとかを実行しモチベーションを維持するのは、どうにも難しいように思えます。 僕の最近の実体験でいうと、去年電子ピアノを購入して趣味で弾いてみていたのですが、うまくモチベーションを管理できず、現在では電子ピアノは触れられることなく部屋を彩るオサレなインテリアのポジションに収まってしまっています(モッタイナイ!) プライベートでやってる作業なんてものは、多くの場合できなかった場合でも人に迷惑を掛けることもなく義務感も生じにくいため、「やめた」と言いやすいですよねー、困ったことに。

さて最近週報仲間なんてエントリを読みまして、これがモチベーション維持に使えそうなんじゃないかなーと思っています。(この話、最近の Rebuild.fm でも紹介されてた気がする!) 今のところ仲間内で毎週日曜に一週間内でやったこと、翌週やること、その他雑感を書いていって、各週の計画や振り返りを共有することによって、そこはかとない作業への義務感を自身に植えつけられ、作業実行やモチベーション維持をスムーズに行える効果が期待できるかなーと。 ......実際のところ、この手法が自分に合っているかはしばらく継続してみないと分からないので、まずは取り組んでみて 1,2 ヶ月後くらいに効果を評価してみようと考え中です。

HTTP/2 のコネクション再利用について確認してみる

はじめに

本記事は http2 Advent Calendar 2015 の 12 日目の記事となります。

本記事では HTTP/2 における TCP コネクション再利用とその周辺仕様を確認してみようと思います。コネクション再利用の挙動を理解することは、実際の Web サイトにおける HTTP/2 のデプロイの助けになると思われます。

HTTP/1.1, HTTP/2 での TCP コネクション管理

よく訓練された方々には既知のことかとは思いますが、 HTTP/2 において HTTP/1.1 の頃にあった性能向上の為のハックであるドメインシャーディングは好ましくないテクニックになってしまいます。 HTTP/1.1 において今日のブラウザは 1 ドメインあたり概ね 6 TCP コネクションほど張って、並列にリクエストを送るような振る舞いをします。このため Web サイトなどで参照するドメインを分割することで HTTP リクエストの並列度を向上する手法がよく取られていました。 しかしながら HTTP/2 において HTTP リクエストの処理は 1 本の TCP コネクションの上で、ストリームという論理単位で HTTP リクエストが並列処理されるようになり、このようなハックは不要になりました。むしろ参照するドメイン名が集約されていた方が、 TCP コネクションを張る回数が削減できて性能の向上に貢献できます。

これは悩ましい問題です。今日までに HTTP/1.1 を前提としたチューニングがされた Web ページの構造を HTTP/2 向けに構築し直すのは骨が折れる作業といえます。また、依然として HTTP/1.1 ユーザも数多くいるわけで、現在 HTTP/2 前提の Web ページ構成にしてしまって良いのかどうかの判断も難しいといえます。 本記事ではこの課題に対する一つの対策となりうる、 HTTP/2 のコネクション再利用呼ばれる振る舞いについて記述します。

HTTP/2 におけるコネクション再利用

RFC7540 Section 9.1.1 Connection Reuse の節には下のような記述があります。

   Connections that are made to an origin server, either directly or
   through a tunnel created using the CONNECT method (Section 8.3), MAY
   be reused for requests with multiple different URI authority
   components.  A connection can be reused as long as the origin server
   is authoritative (Section 10.1).  For TCP connections without TLS,
   this depends on the host having resolved to the same IP address.

   For "https" resources, connection reuse additionally depends on
   having a certificate that is valid for the host in the URI.  The
   certificate presented by the server MUST satisfy any checks that the
   client would perform when forming a new TLS connection for the host
   in the URI.

https://tools.ietf.org/html/rfc7540#section-9.1.1

HTTP/2 において、 オリジンが authoritative であれば既に張られているコネクションを再利用できるのです。 再利用できるかどうかの基準については、再利用しようとしているコネクションにおいて、リクエスト対象のドメイン名がサーバ証明書の subjectAltName にマッチするかどうかで判断します。 例えば、まず https://a.example.com/ にリクエストを投げるために a.example.com にコネクションを張った際にサーバ証明書の subjectAltName に "*.example.com" が含まれている場合、続いて https://b.example.com/ にリクエストを投げる際に以前使ったコネクションを再利用することができます。

f:id:syu_cream:20151211224939p:plain

この仕様により、冒頭で述べた HTTP/1.1 の既存のハックの改修を行わずに HTTP/2 のコネクション集約の恩恵を受けられる可能性が出てきます。 これを利用して、例えば利用ドメインと証明書を集約して持つエッジサーバなどを構築し、 TCP コネクションを束ねて受けてしまうなどの対応が行えます。

Chrome における コネクション再利用

Google Chrome は既にコネクション再利用のロジックが入っているようです。いくつかの Google の Web ページを閲覧した後 HTTP/2 セッションのステータスを chrome://net-internals/#http2 で確認したところ、下記のようになっていました。

f:id:syu_cream:20151211214255p:plain

いくつかのドメイン名の HTTP/2 セッションが集約されていることがわかります。 ただし、 サーバ証明書の subjectAltName としては *.google.com が含まれているのですが、これにマッチして集約されることが期待されるいくつかのセッションが分割されています。

コネクション再利用の落とし穴

このコネクション再利用ですが、サーバとしては望まないコネクションの再利用が行われる可能性があります。 RFC7540 で記述されている通り SNI を元にリクエスト転送先オリジンサーバを決定している HTTP/2 プロキシサーバが存在するかもしれませんし、サーバ証明書としてはワイルドカードの subjectAltName を持っているものの、実際にはレスポンスを返せない場合があります。 このような場合には、サーバはクライアントに対し 421(Misdirected Request) を送って通知することが可能です。

f:id:syu_cream:20151211224943p:plain

ただし、クライアントがこの挙動を考慮して「コネクションを再利用してリクエストを投げてみて 421 が返されてから、再度コネクションを張り直してリクエストを投げ直す」ような挙動をした場合、もちろんレイテンシが向上してしまいます。 この問題を解決するために、 Origin フレーム という HTTP/2 の拡張フレームの Internet-Draft も存在します。 Origin フレームを用いることでサーバ証明書としては集約されてしまうオリジンがあったとしても、サーバが実際に authoritative なオリジンを指定することができ、これにより上記のレイテンシ向上問題を回避することができます。 (Origin フレームは拡張仕様であるためサーバ、クライアント両者が対応していなければならず、また大量のオリジンに対して authoritative であるサーバは Origin フレームにそれらを列挙し送信すると、その送信自体のコストが大きくなってしまいそうな欠点はありますが。。。)

f:id:syu_cream:20151211224947p:plain

おわりに

HTTP/2 のコネクション再利用が期待できると、現在の Web にデプロイした際に HTTP/2 の恩恵が受け易くなると思われます。 ただし現状クライアントの実装と周辺仕様の充実、さらに実際に運用した上で得られた知見が必要である状態だと思われます。

Apache Traffic Server の設定やちょっとした制御ロジックを mruby で書けるようになるかもしれないプラグイン書いてる

mod_mrubyngx_mrubyh2o_mrubylibvmod_mruby など、さまざまな HTTP サーバの設定などを mruby スクリプトで書ける世の中になりつつありますね。 そんな風潮の後押しもあり、近頃、題意の通り mruby で ATS(Apache Traffic Server) の設定やヘッダの加工などのロジックを mruby で記述可能にするプラグインts_mruby をちまちまと開発しています。

ts_mruby によって例えば下記のようなロジックが mruby で書けるようになります。

  • リクエストヘッダを付与したり削除する
if server_name == "NGINX"
  Server = Nginx
elsif server_name == "Apache"
  Server = Apache
elsif server_name == "ApacheTrafficServer"
  Server = ATS
end

# rewrite request headers
r = Server::Request.new
conn = Server::Connection.new
r.headers_in["X-ATS-Plugin"] = "ts_mruby"
r.headers_in["X-Forwarded-For"] = conn.remote_ip
r.headers_in.delete("Cookie")
  • 特定 IP 以外からのリクエストを弾く
if server_name == "NGINX"
  Server = Nginx
elsif server_name == "Apache"
  Server = Apache
elsif server_name == "ApacheTrafficServer"
  Server = ATS
end

whitelist = [
  "127.0.0.1"
]

# deny if client IP isn't listed in whitelist
conn = Server::Connection.new
unless whitelist.include?(conn.remote_ip)
  Server::echo "Your access is not allowed ..."
  Server::return Server::HTTP_FORBIDDEN
end

ATS でプラグインをロードする際、グローバルプラグイン(ATS 全体に作用するプラグイン)や Remap プラグイン(各リクエストをプロキシする際に作用するプラグイン)という形態を採ることになるのですが、 ts_mruby ではそのどちらもサポートしています。 全リクエストで動作させたいのか、特定 VirtualHost や パスだけに対して動作させたいのか、など要件によって使い分けることが可能です。

  • グローバルプラグインとして使う場合、 plugin.config に下のように記述
# write here if you hope to apply all of requests
ts_mruby.so etc/trafficserver/unified_hello.rb
  • Remap プラグインとして使う場合、 remap.config に下のように記述
# write here if you hope to apply only accesses to /test
map /test http://127.0.0.1/ @plugin=ts_mruby.so @pparam=etc/trafficserver/unified_hello.rb

現状サポートしている ts_mruby の mruby 向けクラス、メソッド は mod_mruby, ngx_mruby と共通の名前、機能にするよう気をつけています。 mruby を HTTP サーバ設定の DSL として使うこともできるようになるかもしれません(ただし各サーバ固有の機能の部分はどうしても出てくるわけで、その部分をどう扱うか悩みどころです)

ts_mruby は現状、細々と個人で開発をしている状態です。なにかご意見や Pull-Request などいただけると幸いです。

JMeter で HTTP/2 リクエストを送れるようにするプラグインを書いてみた

表題の通り、 JMeter で HTTP/2 リクエストを送ることを可能にする HTTP2 サンプラーを追加するプラグインを書いてみました。

github.com

但し、今のところ単純な GET リクエストを、しかも 1 request / connection で送る程度のことしかできず、パフォーマンステストをする上で必要な機能が十分にあるとは言えない状態です。 HTTP ヘッダマネージャによるリクエストヘッダの編集、アサーションでのレスポンスコード、レスポンスメッセージ、レスポンスヘッダのチェックはできるので、何か単純なサーバの動作確認を実施する程度になら使えるかも知れません。

プラグインを導入すると、下図のように HTTP/2 Sampler という項目が追加されます。 HTTP/2 Sampler を追加して項目を埋めていくことで HTTP/2 リクエストの送信が実現できます。 f:id:syu_cream:20150706002423p:plain

HTTP サンプラーで使われるヘッダマネージャを、 HTTP/2 Sampler でも使用できます。 f:id:syu_cream:20150706002039p:plain

HTTP/2 Sampler では現在、サーバの IP 、ポート、パスを指定できるようにしています。 今後必要な項目を用意していきたいところです。(レイアウトが残念なことになっているのでそれもどうにかしたい f:id:syu_cream:20150706002832p:plain

テスト結果はアサーションで検査したりリスナーで一覧、保存できたりします。 jtl ファイルに出力させれば、 Jenkins の Performance Plugin に解釈させることも可能なはず。 f:id:syu_cream:20150706002817p:plain

RFC 7541 (HPACK) の日本語訳を公開しました

表題の通り、 RFC7541 "HPACK: Header Compression for HTTP/2"日本語訳されたドキュメント を公開しました。 今のところ GitHub の僕のリポジトリにて編集管理を行っています。(将来的には別の管理の仕方をするかもしれません)

前回日本語訳を行った時点(draft-10) から RFC になるまでの、記述上の細かい修正の追従が主になります。また、前回イマイチだった箇所の日本語訳も少し修正しています。

問題点の指摘や翻訳の改善の Pull-Request 、大歓迎です。 こちら よりお願いします。

末筆ですが、やっと HTTP/2 , HPACK の RFC が出ましたね!これまで変更を追従してきた皆様お疲れ様でした。