Showing posts with label H2O+日本語. Show all posts
Showing posts with label H2O+日本語. Show all posts

Thursday, June 18, 2020

QUICむけにAES-GCM実装を最適化した話 (2/2)

前半で述べたように、OpenSSLのAEAD暗号器は、長いAEADブロックの処理を前提に作られています。平文の暗号化処理においては理論上の上限にあたる速度を叩き出す一方、事前処理と事後処理、および呼出オーバーヘッドについては、あまり最適化が図られているとは言えません。これは、AEAD暗号の主な使用用途が、これまでTLSという長いAEADブロックを使う(ことが一般的な)プロトコルであったことを反映していると言えるでしょう。

一方、QUICにおいては、UDPパケット毎に独立した、短いAEADブロックを暗号化する必要があり、したがって、次のような速度向上の機会があることが分かります。

AEAD処理をひとつの関数にまとめ、事前処理と事後処理を、パイプライン化されスティッチングされた暗号処理と並行に走らせることができれば、AEADブロックが短くても、理論値に近いスループットを発揮するような、AES-GCM実装を作ることができる(前半より引用)

この条件を満たすような関数を実装し、ボトルネックをつぶしていって速度向上を図るというのは一案です。しかし、往々にして、そのような対症療法的なプログラミングスタイルでは、何回もの変更に伴う手戻りが発生したり、必ずしも最適でないコードが成果物の一部に残ったりしがちです。

より効率的な設計手法はないものでしょうか。

■QUIC向けAES-GCM実装「fusion」の設計方針

幸いなことに、AES-GCMについては、第9世代Core CPUにおけるボトルネックがAES-NIであり、そのスループットの理論上の上限が64バイト/40クロックであることが分かっています。スティッチングを用いたAES-GCM実装が、暗号化処理中、AES-NIを最高速度で回しつつ、他の演算ユニットを用いてGCMのハッシュ計算を行うという手法であることも、先に述べたとおりです。

ならば、AES-NIを常時実行しつつ、その合間をぬって、AEADの事前処理、事後処理を含む他のあらゆる処理を行うようにすれば、理論上の上限値に迫るようなAES-GCM実装が作れるのではないでしょうか。

このような考えに基づき、以下のような特徴をもつAES-GCM暗号ライブラリ「fusion」を作成することにしました:
  • できるだけ長い間、6*16バイト単位でAES-NIを実行する
  • その間に、AAD(=事前処理)を含む、任意の長さのGCMハッシュ計算を行う
  • 複雑な設計をメンテ可能とするために、アセンブリではなくCで記述する
  • AEADブロック全体にわたって、GCMハッシュの事前計算を行う。それにより、reductionの負荷を下げる
  • パケットヘッダ暗号化(パケット番号暗号化)に必要なAES演算を重畳する

AES-GCM暗号化の典型的なデータフローを可視化してみましょう。第一の図が、古典的な(OpenSSLのような)暗号化部分に注力したアプローチです。第二の図が、fusionのアプローチです。横軸が時間軸で、縦に並んでいる処理は同時実行(スティッチング)されています。fusionでは、より多くの処理がスティッチングされることがわかります。



以下が、fusion.cの暗号化のホットループです。gfmul_onestepは、1ブロック分のGCMハッシュの乗算演算を行うインライン関数です。6ブロック分(bits0〜bits5)のAES計算をする間に、gdata_cntで指定された回数だけgfmul_onestepを呼び出していることがわかります。
#define AESECB6_UPDATE(i) \
    do { \
        __m128i k = ctx->ecb.keys[i]; \
        bits0 = _mm_aesenc_si128(bits0, k); \
        bits1 = _mm_aesenc_si128(bits1, k); \
        bits2 = _mm_aesenc_si128(bits2, k); \
        bits3 = _mm_aesenc_si128(bits3, k); \
        bits4 = _mm_aesenc_si128(bits4, bits4keys[i]); \
        bits5 = _mm_aesenc_si128(bits5, k); \
    } while (0)
#define AESECB6_FINAL(i) \
    do { \
        __m128i k = ctx->ecb.keys[i]; \
        bits0 = _mm_aesenclast_si128(bits0, k); \
        bits1 = _mm_aesenclast_si128(bits1, k); \
        bits2 = _mm_aesenclast_si128(bits2, k); \
        bits3 = _mm_aesenclast_si128(bits3, k); \
        bits4 = _mm_aesenclast_si128(bits4, bits4keys[i]); \
        bits5 = _mm_aesenclast_si128(bits5, k); \
    } while (0)

    /* run AES and multiplication in parallel */
    size_t i;
    for (i = 2; i < gdata_cnt + 2; ++i) {
        AESECB6_UPDATE(i);
        gfmul_onestep(&gstate, _mm_loadu_si128(gdata++),
                      --ghash_precompute);
    }
    for (; i < ctx->ecb.rounds; ++i)
        AESECB6_UPDATE(i);
    AESECB6_FINAL(i);

コードを注意深く読んだ方は、bits4の計算だけ、異なる鍵を使うようになっていることに気づいたかもしれません。これが、パケットヘッダ暗号化のためのAES計算を重畳するための工夫です。

■パケットヘッダ暗号化

パケットヘッダ(パケット番号)の暗号化は、QUICやDTLS 1.3といった新世代のトランスポートプロトコルに見られる機能です。パケットヘッダを暗号化することで、傍受者による通信内容の推測をより難しくしたり、中継装置(ルータ)が特定の通信パターンを前提にしてしまうことによりトランスポートプロトコルの改良が困難になること(ossification)を防ぐ効果が期待されています。

なぜ、パケットヘッダ暗号化のAES計算を重畳するのか。それは、6ブロック分のAES計算を一度に行う以上、パケット長を96で割った余りが65から80の間にならない限り、使われないスロットが発生するためです。その余ったスロットをパケットヘッダ暗号化のAES演算に使うことで、パケットヘッダ暗号化のコストを隠蔽するのが目的です。

パケットヘッダ暗号化を重畳した場合のデータフローを、以下に示します。

■ベンチマーク

では、ベンチマーク結果を見てみましょう。

青の棒は、OpenSSLのAES-GCM処理のうち、事前処理と事後処理を含まないスループットを、赤の棒は、両者を含んだトータルでのスループットを表しています。黄色はfusionのトータルスループット、緑は、パケットヘッダ暗号化に必要な演算を重畳した場合の値です。


まずは、最近のIntel製CPUである、Core i5 9400の値を見てみましょう。

AEADブロックサイズが16KBの場合、OpenSSLの事前事後処理を含まないスループットとfusionのスループットが、いずれも6.4GB/sという理論上の上限に達していることが分かります(微妙なズレは、CPUクロック制御の精度に起因するものです)。OpenSSLの事前事後処理を含むスループットは若干遅い6.2GB/sですが、TLSにおいて、事前事後処理を最適化しないオーバーヘッドは3%以下である、という風に読むこともできます。

一方で、AEADブロックサイズが1440バイトの場合、差は顕著です。OpenSSLのトータルスループットが4.4GB/sと、理論値の約70%にまで落ち込むのに対し、fusionは理論値の90%を超えるスループットを発揮します。また、パケットヘッダ暗号化によるオーバーヘッドが1%以下なのも見てとることができます。

AMD Ryzenに目を向けると、AEADブロックサイズ1440バイトの場合のみならず16KBの場合でも、fusionが勝っていることが読み取れます。これは、RyzenのAES-NIのスループットがPCLMULと比較して高いため、ボトルネックがPCLMULに代表されるGCMハッシュ計算の側に移動したものと考えられます。fusionは、想定されるAEADブロック全体にわたって事前計算を行うことで、GCMハッシュ演算のうちreductionの回数を削減しているので、ブロックサイズ16KBの場合にも差がついたと考えることができます。

■考察

カーネル・ネットワークカードのUDP処理が最適化された場合、暗号処理のコスト差が問題となって、TLSよりもQUICのほうがCPU負荷が高くなる、という問題がありました。この問題について、QUICを始めとする暗号化トランスポート向けに最適化したAES-GCM実装を準備することで、大幅な改善が可能であることを示しました。fusionをQUICの暗号ライブラリとして使った場合の詳細は本稿では紹介しませんが、TCPとUDPでGSOハードウェアオフロードがある環境において、パケットサイズ9KBならQUICが優位、パケットサイズ1.5KBでもQUICのオーバーヘッドはTLS+5%程度だという測定結果を得ています(参照: h2o/quicly PR #359)。

あわせて、
  • パケットヘッダ暗号化のコストは(少なくとも送信側においては)特に問題視するレベルではないこと
  • アセンブリを用いる場合と比較して、C言語を用いることで、最善ケースのスループットを保ったまま、より高度な設計による暗号ライブラリが開発可能であること
を示しました。

今回開発したAES-GCM実装「fusion」は、昨日、我々が管理するTLSスタックであるpicotlsにマージされ、使用可能になっています。fusion、あるいはそれに類する実装手法を用いることで、インターネット上の通信が、より低コストに、より安全になっていくことを期待します。

末筆ですが、fusionを開発するにあたり、光成(@herumi)さんにアドバイスを、吉田(@syohex)さんにベンチマークでご協力をいただきました。この場を借りて御礼申し上げます。

Monday, June 15, 2020

QUICむけにAES-GCM実装を最適化した話 (1/2)

4月末に、会社のほうで「Can QUIC match TCP’s computational efficiency?」というブログエントリを書きました。我々が開発中のQUIC実装であるquiclyのチューニングを通して、QUICのCPU負荷はTLS over TCP並に低減可能であろうと推論した記事です。この記事を書く際には、Stay Homeという状況の中で、手元にあった安いハードウェアを使ったのですが、その後、10gbe NICを入手し、ハードウェアによるUDP GSOオフロード環境でのパフォーマンスを確認していくと、OpenSSLのAES-GCM実装がボトルネックになることがわかってきました。

TCP上で通信するTLSでは、一般に、データを16KB単位でAEADブロックに分割して、AES-GCMを用いてAEAD暗号化します注。一方、UDPを用いるQUICでは、パケット毎にAES-GCMを適用することになります。インターネットを通過することができるパケットサイズは高々1.5KBなので、QUICのAEADブロックサイズはTLSと比較して1/10以下となります。

両条件について、OpenSSLのAES-GCM実装のスループットを比較したところ、4GHzの第9世代Intel Core CPUを使った場合、AEADブロックサイズ16KBにおいては約6.4GB/sなのに対し、AEADブロックサイズ1440バイトにおいては、4.4GB/s程度しか出ないことが分かりました。ハードウェアGSOオフロードが可能な環境ではCPU負荷の半分弱が暗号処理コストになるので、暗号処理で7割のスループットしか出ないのは、QUICの足かせになります。

それにしても、なぜ、これほど大きな速度差が発生するのでしょう。

その答えを理解するには、最適化されたAES-GCM実装が、一般に、どのようなものかを知っておく必要があります。

■AES-NIとパイプライン処理

まず、AES-GCMのうち、暗号処理であるAES実装の最適化手法を見てみましょう。

最近のx86 CPUは、たいてい、AES-NIというAES処理専用の命令に対応しています。128bitのAES暗号化においては、AES-NI命令を10回発行することで、16バイトの暗号化を行うことが可能です。第9世代のIntel Core CPUにおいては、AES-NI命令のレイテンシは4クロックなので、10*4=40クロックで16バイトの暗号化が可能になります。

4GHzでのスループットを計算してみると、4GHz / 40clock * 16byte = 1.6GB/sになります。

あれ、先ほどの6.4GB/sと比べると1/4の値です。なぜでしょう?

実は、x86 CPUはAES-NIをパイプライン処理します。そのため、依存関係のない(=別のAESブロックを処理する)AES-NI命令を1クロックごとに1個発行することが可能なのです。

つまり、16バイトの基本ブロック単位で処理するのではなく、AES-NI(ブロック1用)、AES-NI(ブロック2用)、AES-NI(ブロック3用)、AES-NI(ブロック4用)、AES-NI(ブロック1用)、...のように、16バイトのブロック4つ分のAES-NI命令を並行に発行し続けることで、64バイト分の暗号化を4*10=40クロックで終えていくことができるのです。

4GHzでのスループットを再び計算すると、$GHz / 40clock * 16byte * 4 = 6.4GB/s。

毎クロック1命令発行、16バイトの暗号化に10命令必要ですから、これが理論上の上限値になります。

でも、ちょっと待ってください。6.4GB/sは、暗号処理であるAESのスループットであって、認証符号であるGCMの負荷を含んでいません。GCMの負荷は一体どこに行ってしまったのでしょう。

■AES-GCMのスティッチング

GCMはガロア体における乗算を用いる認証符号で、x86 CPUでは、PCLMULQDQというキャリーレス乗算命令を利用する最適化が知られています。さらに、式を変形することで、16バイトあたりのPCLMULQDQ命令発行回数を5回に減らせることが、また、事前計算を行えば、16*nバイトあたりのPCLMULQDQ命令発行回数を3*n+2回まで減らせることが知られています(参照: https://crypto.stanford.edu/RealWorldCrypto/slides/gueron.pdf)。

PCLMULQDQ命令は、7クロックのレイテンシがありますが、AES-NIとは同時に発行可能なので、AES命令とPCLMULQDQをほどよく織り交ぜるようなプログラムを書くことで、AESとGCMを並列に計算することが可能です。

IntelのCPUにおいては、若干GCMの方がAESよりも軽いので、ボトルネックはAESになり、AES-GCMでもAES同様のスループット6.4GB/sが期待できます。

■OpenSSLのaesni_ctr32_ghash_6x関数

以上を踏まえ、OpenSSLのAES-GCM実装であるaesni_ctr32_ghash_6x関数を見てみましょう。

この関数は、perlスクリプトを用いて生成されるアセンブリコードですが、以下のような、AES-NI命令とPCLMULQDQ命令が織り混ざる構成になっています。また、AES-NI命令について、同じラウンドキーを違う引数($inout)に適用している、つまり、複数ブロックの暗号化を同時に行なっていることが分かります。命令の目的によってインデントを変えるなどの工夫も興味深いところです。

vpclmulqdq \$0x01,$Hkey,$Ii,$T2
    lea  ($in0,%r12),$in0
      vaesenc $rndkey,$inout0,$inout0
     vpxor 16+8(%rsp),$Xi,$Xi # modulo-scheduled [vpxor $Z3,$Xi,$Xi]
    vpclmulqdq \$0x11,$Hkey,$Ii,$Hkey
     vmovdqu 0x40+8(%rsp),$Ii # I[3]
      vaesenc $rndkey,$inout1,$inout1
    movbe 0x58($in0),%r13
      vaesenc $rndkey,$inout2,$inout2
    movbe 0x50($in0),%r12
      vaesenc $rndkey,$inout3,$inout3
    mov  %r13,0x20+8(%rsp)
      vaesenc $rndkey,$inout4,$inout4
    mov  %r12,0x28+8(%rsp)
    vmovdqu 0x30-0x20($Xip),$Z1 # borrow $Z1 for $Hkey^3
      vaesenc $rndkey,$inout5,$inout5
注意深い方は既にお気づきかもしれませんが、関数名の6xは、128bitのブロックを6ブロック単位で(つまり、96バイト単位で)AES-GCM符号化を行なっていることに由来します。

■なぜOpenSSLは短いAEADブロックの処理が苦手なのか

このように、丁寧に最適化されたコードであるにもかかわらず、なぜ、OpenSSLは短いAEADブロックの処理が苦手なのでしょうか。2つの要因が考えられます。

第一の要因は、関数呼び出しのオーバーヘッドです。OpenSSLのAEAD処理は、EVPと呼ばれるレイヤで抽象化されています。ひとつのAEADブロックを暗号化するには、EVP_EncryptInit_ex、EVP_EncryptUpdate、ENP_EncryptFinal、という3つの関数を介して、AES-GCM固有の処理を呼び出す必要があります。

第二の要因は、AES-GCMの事前処理と事後処理がパイプライン化されていない点です。先に紹介したaesni_ctr32_ghash_6x関数は、6.4GB/sという理論値を叩き出す、文句のつけどころのない関数です。しかし、AES-GCMにおいては、暗号化以外にも、AEADブロック毎に、AAD(認証つき平文)をGCMのコンテクストに入力したり、最終的なタグを計算するなどの処理が必要です。これらの付随する処理の負荷は、AEADブロックサイズが小さくのればなるほど、相対的に大きくなります。

これらの問題を指摘することは簡単です。

なるほど、AEAD処理をひとつの関数にまとめ、事前処理と事後処理を、パイプライン化されスティッチングされた暗号処理と並行に走らせることができれば、AEADブロックが短くても、理論値に近いスループットを発揮するような、AES-GCM実装を作ることができるでしょう。

しかし、上述したように、パイプライン化・スティッチングされたコードは、既に相当複雑です。ここにさらに、事前処理や事後処理を重畳することなどできるでしょうか。できたとして、保守可能なプログラムになるでしょうか。


次回に続きます。



注: スロースタート時には、より小さなブロックサイズを使って実効レイテンシを改善する場合もあります(参照: https://www.slideshare.net/kazuho/programming-tcp-for-responsiveness)

Saturday, September 8, 2018

次世代プロトコル(QUIC etc.)のセキュリティとプライバシー @ #builderscon

9月6日より開催中の builderscon 2018 において、登壇の機会をいただき、インターネットのトランスポート層プロトコルについてセキュリティやプライバシーに関わる設計がどのように進めてられているか、TLS と QUIC を中心に発表しました。

QUIC のハンドシェイクプロトコルとパケット番号暗号化、TLS の Encrypted SNI 拡張は、いずれも僕が提案した機能あるいは方式が採用される予定のものなので、背景にある動機や意義を含め、整理して発表する機会をもらえたことをありがたく感じています。

聴講いただいた方々、また、スライドをご覧になる方々と、次世代プロトコルの暗号応用の手法のみならず意義を含め共有し、理解と議論を深めることができれば、これに勝る喜びはありません。



PS. QUIC のハンドシェイクプロトコルと Encrypted SNI 拡張については、以下のブログ記事もあわせてご覧いただけます。

QUICハンドシェイクの再設計、もしくはTLSレイヤの終焉
TLS の SNI 暗号化に関する Internet Draft を共同提出しました

Tuesday, April 17, 2018

HTTP/2で 速くなるとき ならないとき

たいへん遅ればせながら、YAPC::Okinawa 2018 ONNNASONで使用したスライドを、こちらにて公開する次第です。

ベンチマークの難しさとチューニングの奥深さ、楽しさを共有できた結果がベストトーク賞につながったのかなと考えています。ありがとうございました&今後ともよろしくお願いいたします。

Thursday, January 12, 2017

Fastly に入社しました

Summary in English: Joined Fastly, will continue my work on H2O there as an open-source developer.

2017年1月1日付で、Fastly 社へ転職したので報告いたします。

過去5年間、DeNA では R&D 的な立場から、様々な基盤的ソフトウェア(オープンソースになったものもありますし、クローズドなものもあります)の開発に携わってきました。

最近2年間は、同社のゲーム用サーバに端を発するオープンソースの HTTP/2 サーバ「H2O」の開発に従事してきましたが、その実装品質が高く評価され、世界有数のコンテンツ配信ネットワーク(CDN)である Fastly で採用された他、大規模なウェブサービス事業者で採用にむけた動きが進むなどの成果が出つつあります。
また、H2O における実装経験をもとに、HTTP プロトコルの拡張をインターネットプロトコルの標準化機関である IETF に提案し、ワーキンググループでの検討が行われるという状況にもなってきています注1。

ソフトウェアの技術開発で世の中を前に進めようとするならば、ただコードを書くだけでは不足です。使い手にとって便利なように、実地において効率良く動作するように、改善していくことが必要不可欠です。標準化プロセスにおいても、多様な実在するワークロードを元に効果を証明できることが、説得力の点で重要になります。

これらの点に鑑みると、H2O という育ちつつある芽を大きく花咲かせるために、自分が今、身を置くべき場所は、DeNA ではなく、H2O の世界最大の利用者であり、世界有数の規模の HTTP トラフィックを捌く事業者であり、HTTP を高度に運用することを事業のコアとしている Fastly なのではないかと考え注2、転職を決意するに至りました。

転職したといっても、このような経緯なので、職務の内容が何か変わるわけではありません。これまでと同様に、オープンソースソフトウェアである H2O の開発をリードしていくのが僕の役割になります。Fastly 社内でしか得られない知見や実験の成果もオープンソースとして H2O に還元され、あるいはプロトコルの拡張として標準化を提案していく予定です。

今後ともよろしくお願いいたします。引き続き東京ベースで活動しますので、何かありましたら気楽にお声がけください。

また、末筆になりますが、H2O の開発をこれまで支え、笑顔で送り出してくれた DeNA の上司と同僚にはありがとうを伝えたいと思います。開発は DeNA 社内でも引き続き行われます。

注1: Cache Digests for HTTP/2, Call for Adoption: Early Hints (103)
注2: HTTP サーバのどの側面に注力したいかによって摂るべき選択肢は変わると思いますが、僕の場合、最近の活動はサーバとアプリケーションの間よりも、サーバとクライアントの間の通信改善に関わるものが多くなってきていました。

Monday, December 12, 2016

103 Early Hints に対応した Starlet 0.31 をリリースしました

Perl のウェブアプリケーションサーバである Starlet の新バージョン、0.31をリリースしました。

今回搭載された新機能は 100 番台の中間レスポンスの送信に対応した点です。

たとえば以下のような感じで 103 Early Hints レスポンスを送信することで、アプリケーションでリクエストを処理する前に、HTTP/2 リバースプロキシに関連アセットのプッシュの開始を指示することができます注。

sub {
    my $env = shift;
    $env["psgix.informational"}->(103, [
      'link' => '; rel=preload'
    ]);
    my $resp = ... application logic ...
    $resp;
}

Early Hints は、現在 IETF の HTTP WG で Call for Adoption を迎えている段階ですが、H2O version 2.1-beta のほか、nghttpx、Apache HTTP Server の HTTP/2 実装である mod_h2 の trunk などが既に対応しています。

注: HTTP クライアントによっては中間レスポンスの処理にバグを抱えている可能性があるため、Early Hints の使用はリバースプロキシを挟んでいる場合に限ることを現時点ではおすすめします。

Saturday, December 10, 2016

HTTP/2の課題と将来について、YAPC Hokkaidoで話してきた

スライドは以下になります。内容は先月のVelocity in Amsterdamでの発表と、それ以降のアップデートですが、スライドが日本語になっているので、日本人の方にはこちらのほうが良いかと思います。

Wednesday, November 11, 2015

mruby で同期呼出を非同期化する話(もしくは H2O の mruby ハンドラでネットワークアクセスする話)

■背景

H2Oではバージョン1.5より、mrubyを用い、Rackのインターフェイスに則った形でハンドラを書けるようになっています。

この機能を提供している目的は、正規表現による書き換え等を用いる複雑な設定ファイルではなくプログラミング言語を用いることで、ウェブサーバの設定をより簡潔に拡張しやすくするためです(Apacheのmod_rubyやmod_perlのようにウェブアプリケーションをウェブサーバ内で実行可能にすることではありません)。

とは言っても、現実のウェブサーバの設定においては、外部のデータベース等に問い合わせた結果に基づいたルーティングが必要になることがあります。

H2Oのようなイベントドリブンなウェブサーバ上で動作する、同期モデルを採用するRackインターフェイスを用いて記述されるハンドラ内において、データベースへの問い合わせをどのように実現すれば良いか。問い合わせが同期的だと、その間ウェブサーバの処理が止まってしまうので、Rubyで問い合わせ関数が呼ばれたタイミングで、ウェブサーバ側に処理を戻したいわけです。

そんなこんなで
とツイートしたところ、
という流れになりました。

その後、考えたところ、
という気がしてきたので、まずはPoCを書いてみることにしました。

■Fiberを使って、同期コールを非同期化するPoC

ざっと、以下のような感じになります。Rack ハンドラ自体を Fiber 内に置き、その入出力と、非同期化したい関数(ここでは DB#query)が呼ばれたタイミングで Fiber.yield を呼ぶことで、メインループ(これは実際には C で書くことになる)へ制御を戻しています。

# DB class that calls yield
class DB
  def query
    return Fiber.yield ["db#query"]
  end
end

# the application, written as an ordinary Rack handler
app = lambda {|env|
  p "received request to #{env["PATH_INFO"]}"
  [200, {}, ["hello " + DB.new.query]]
}

# fiber that runs the app
runner = Fiber.new {
  req = Fiber.yield
  while 1
    resp = app.call(req)
    req = Fiber.yield ["response", resp]
  end
}
runner.resume

# the app to be written in C
msg = {"PATH_INFO"=> "/abc"} # set request obj
while 1    
  status = runner.resume(msg)
  if status[0] == "response"
    resp = status[1]
    break
  elsif status[0] == "db#query"
    # is a database query, return the result
    msg = ""
  else
    raise "unexpected status:#{status[0]}"
  end
end
p "response:" + resp[2].join("")

やろうと思えばできることはわかりました。しかし、この手法には制限が2点あります。
  • fiber 内からしか呼べない - それでいいのか?
  • fiber 内で、Cコードを経由して呼ばれた ruby コードから Fiber.yield できない
いずれも大した問題ではないですが、ここに付記しておきます(後者は mruby の場合、大きな問題にならないと認識されているようです。参照: twitter.com/yukihiro_matz/status/664276538574049280)。

■プロトコルバインディングの実装手法

さて、これで行けそうだということは分かったのですが、可能であることと、それが良いアプローチであることが等価であるとは限りません。そもそも、プロトコルバインディングはどのように書かれるべきなのでしょうか。2種類に大別してプロコンを書きたいと思います。

  • Cライブラリのラッパーを書く
    • Cライブラリが、非同期モデルをサポートしている必要がある
    • イベントループ (libuv, libev, ...) 毎に対応が必要
    • プロトコルを実装しなくて良い
  • rubyでバインディングを書く
    • プロトコルを実装する必要がある
    • rubyで書ける!
    • 各バックエンド (libuv, libev, ngx_mruby, h2o, ...) が同じ ruby API (TCPSocketのサブセットで良いと思う) を提供すれば、イベントループ毎の対応が不要
    • Cより遅いかも…

個人的には、rubyでバインディングを書くアプローチが好みです。速度が遅いかも…という点については、Perl IO を用いた HTTP 実装を推進してきた立場から言うと、スクリプト言語のI/Oレイヤの負荷はネットワーク通信を行うプログラムにおいては多くの場合問題にならないと考えます。問題になるとすれば、通信データのパーサですが、ここのみをネイティブコード化するという手法で十分に対応できることは、Plack や Furl に慣れた Perl プログラマであれば納得できる話かと思いますし、(m)ruby においても同等かと思います。

■まとめ

長くなりましたが、H2O (あるいはイベントドリブンなプログラム一般)から、同期的に書かれたネットワーククライアントを呼び出す mruby スクリプトを起動する方法については、
  • 同期的に記述されたアプリケーションを Fiber を使ったラッパーで非同期化する
  • ホストプログラムは、Fiber を通じて、TCPSocket と互換性のある同期ソケット API を提供する
  • プロトコルバインディングは、Rubyで(もしくは、Ruby の TCPSocket と C で書かれたプロトコルパーサを組み合わせて)提供する
という形で行うのが最善ではないかと思いました。

Friday, November 6, 2015

ソート済の整数列を圧縮する件

圧縮されたソート済の整数列ってのは汎用的なデータ構造で、たとえば検索エンジンの転置インデックスとか、いろんなところで使うわけです。で、検索エンジンの場合は速度重要なので、PForDeltaとか様々なデータ構造が研究されてる。

一方、H2O には、ブラウザキャッシュに載ってない js や css をサーバプッシュする仕組み「cache-aware server push」があって、何がキャッシュされているか判定するためにブルームフィルタを全ての HTTP リクエストに含める必要がある。

で、ブルームフィルタを圧縮しようと思うと、ブルームフィルタってのはソート済の整数列として表現できるので、これを圧縮しようって話になる。

検索エンジン等で使う場合は速度重要だけど、HTTPリクエストに載せる場合は空間効率のほうが重要になる。ってことで、空間効率が理論限界に近いゴロム符号(の特殊系であるライス符号)を使うことになる。

ってことで、作ったのがgithub.com/kazuho/golombset。

今週ちょっとcodecをいじって、あと気軽に試せるようにコマンドラインインターフェイスを追加した。

なので、こいつを git clone して make して、以下のような感じで使うことができる。

(100, 155, 931) というソート済の整数列をエンコード
% (echo 100; echo 155; echo 931) | ./golombset --encode | od -t x1
0000000    41  90  6d  c0  ff
0000005

同じ整数列をエンコードしてデコード
% (echo 100; echo 155; echo 931) | ./golombset --encode | ./golombset --decode
100
155
931

(100,155,931)という3つの数値を含むソート済の整数列を5バイトにエンコードできていることがわかる。

もうちょっと実際的な例として、偽陽性が1/100の確率で発生するブルームフィルタに、100個の要素を突っ込むんでエンコードすることを考える。適当にランダムな値を用いてそのようなフィルタを作成しエンコードしてみると、結果が102バイトであることがわかる。

% perl -MList::MoreUtils=uniq -e 'my @a = (); while (@a < 100) { @a = uniq sort { $a <=> $b } (@a, int rand(10000)); } print "$_\n" for @a' | ./golombset --encode | wc -c
     102

つまり、CSSやJavaScriptのような、ブラウザのレンダリングにクリティカルな影響を与えるファイルが100個あるとして、それらがウェブブラウザのキャッシュ内に存在するかを判定するためのブルームフィルタをHTTPリクエストに添付するためのオーバーヘッドは100バイト程度である、ということになる。さらに、リクエストを2回以上繰り返す場合は、HPACKによる圧縮が効く。

以上が、これなら現実的だよねってんで H2O の cache-aware server push は実装されたのでした、という経緯と、それにあわせて作ったライブラリの紹介でした。

それでは、また。

参考:
ImperialViolet - Smaller than Bloom filters
Golomb-coded sets: smaller than Bloom filters (giovanni.bajo.it)

Thursday, October 1, 2015

ウェブページの描画 (first-paint) までの時間を測定するツールを作った件、もしくはHTTP2時代のパフォーマンスチューニングの話

ウェブページの表示までにかかる時間をいかに短くするかってのは、儲かるウェブサイトを構築する上で避けて通れない、とても重要な要素です。

少し古いデータとしては、たとえば、ウェブページの表示が500ミリ秒遅くなると広告売上が1.2%低下するというBingの例なんかも知られているわけです。

「ウェブページの表示までにかかる時間」と言った場合、実際には以下のようないくつかのメトリックがあります。

イベント 意味
unload 現在のページからの離脱。離脱後、first-paintまでは真っ白な表示になります
first-paint ウェブページの初回描画(HTMLの後半や画像は存在しない可能性があります)
DOMContentLoaded ウェブページのレイアウト完了
load (onload) 画像等を含む全データの表示完了

ウェブサイトチューニングにおいては、
  • ユーザができるだけ早くウェブページを閲覧し始めることができるよう、first-paintの値を小さくすることを第一の目標注1
  • 全データができるだけ早く揃うよう、loadの値を小さくすることを第二の目標
とすることが一般的かと思います。

ですが、残念なことに、first-paintまでの時間をAPIを用いて取得できるウェブブラウザは一部に限られています(参照:「Webページ遷移時間のパフォーマンス「First Paint」を計測する方法」)。また、測定にあたって、運用中のウェブページに変更を加えたくない、ということもあったりします。

wpbenchは、これら2点の問題を解消するベンチマークツールです。


wpbenchは、たった1枚のHTMLファイルです。このHTMLファイルを自分のウェブサイトに追加するだけで、そのサイト上の任意のページの表示までにかかる時間が測定可能になります。また、body.clientHeightをポリングするという力技により、FirefoxやChromeのようなfirst-paint取得のためのAPIを提供しないウェブブラウザにおいても、その値を取得、表示することができます。

HTTP/1.1を使っている場合、これらパフォーマンスメトリックは、専らネットワーク状況とウェブブラウザのみに依存して決まる値でした。しかし、HTTP/2では、ウェブブラウザとウェブサーバが恊働して、アセットの転送順序を制御することができるようになった結果、使用するウェブサーバによってパフォーマンスが大幅に変わる、という状況が産まれはじめています。

たとえば、HTTP2サーバ「H2O」のベンチマークページを見ていただくと、ウェブサーバによってfirst-paintまでの時間が倍近く異なるケースがあることがお分かりいただけるかと思います。

既にパフォーマンスチューニング済のウェブサイトを管理している方々も、HTTPS、あるいはHTTP/2への対応にあたり、いま一度、ウェブサイトの表示速度を確認されることをおすすめいたします。


注1: ブラウザによってはfirst-paintのタイミングを取得できないため、DOMContentLoadedを使うこともありますが、DOMContentLoadedには<body>末尾に配置する統計系のスクリプトの読み込みにかかる時間等が加算される点等、注意が必要になります。また、画像なしに閲覧が不可能なサイトについては、Above the foldの値をチューニングする必要があります

Thursday, July 23, 2015

前方秘匿性 (forward secrecy) をもつウェブサイトの正しい設定方法

前方秘匿性(forward secrecy)とは、以下のような性質を指します。

公開鍵暗号の秘密鍵のように、比較的長期に渡って使われる鍵が漏えいしたときでも、それまで通信していた暗号文が解読されないという性質
鍵が漏れることも想定せよ――クラウド時代における「楕円曲線暗号」の必然性 - @IT

鍵が攻撃者や諜報機関など第三者の知るところとなった場合でも、それまで通信していた暗号文が解読されないようにしないといけない、という考え方とともに、最近 HTTPS を利用するウェブサイトにおいても導入が求められるようになってきた概念です。

前方秘匿性を満たすウェブサイトの設定方法については、TLSの暗号化方式をECDH_RSAあるいはECDHE_RSAに設定すれば良い、と述べている文献が多いです。

ですが、ほとんどのウェブサーバにおいて、それは誤りです。

なぜか。

通信を暗号化する鍵(セッション鍵)の確立にDHEあるいはECDHEを用いれば前方秘匿性が確保されるというのは、鍵交換に議論を限ってしまえば正しいです。

しかし、TLSプロトコルには、鍵交換以外にセッション鍵を保存し、再利用する仕組みが存在します。Session ResumptionとSession Ticketの2つがそれに該当します。

このうち、OpenSSLが標準でサポートし、ウェブブラウザが対応している場合(IE9以前とMobile Safari以外の全ての主要ウェブブラウザが該当します)にSession Resumptionより優先して利用されるSession Ticketは、セッション鍵をサーバのみが知るマスターキーで暗号化して、クライアントに送信します。

セッション鍵をマスターキーで暗号化…いやな響きがしませんか?

そうです。このSession Ticketの暗号化に使われるマスターキーを定期的に更新しない限り、マスターキーが流出してしまった場合、過去の通信内容がすべて解読されてしまうことになるのです。

しかし残念なことに、ほとんどのウェブサーバは、マスターキーを自動的に更新する仕組みをそなえていません。

では、どのようにサーバを設定すればいいでしょうか。

解決策1. Session Ticket機能を無効化する

第一の解決策はウェブサーバの設定でSession Ticket機能を無効化することです。Apache の場合は SSLSessionTickets、nginx の場合は ssl_session_tickets という設定コマンドを使うことで、無効化することができます。

しかし、nginx (とevent mpmを使用するApache)は、複数サーバにまたがる session resumption 機能を実装していません。そのため、ロードバランサの背後に複数のHTTPSサーバを立てている場合は、session ticket をオフにすると、新規接続確立のたびにTLSハンドシェイクが発生し、ユーザの体感速度の低下やサーバの負荷増大につながってしまいます。

解決策2. サーバを定期的に再起動する

ApacheやNginxは、サーバを起動するタイミングで Session Ticket のマスターキーを再生成します。なので、たとえば1時間に1回サーバを再起動すれば、前方秘匿性を確保しつつ Session Ticket を使うことができます。簡単ですね!!!

なお、ロードバランサの背後に複数のHTTPSサーバを立てている場合は、それらのサーバを再起動する度に、新しいマスターキーが記述された設定ファイルを再配布する必要があります。このへんは Twitter さんの涙ぐましい努力の話があるので、それを参考にがんばりましょう。

解決策3. H2O を使う

これまで述べてきたように、これまでのウェブサーバの前方秘匿性のサポートはお粗末と言わざるをえません(他にも、マスターキーの強度が強力なセッションキーよりも弱いといった問題も存在します)。

そこで H2O ですよ!!!!

私たちが開発しているH2Oの最新バージョンは、これらの問題を全て解決しています。

具体的に言うと、

・Session Ticket のマスターキーは、他のサーバのように AES-128 ではなく AES-256 です
・Session Ticket は自動的に更新されます(デフォルト15分間隔)
・ロードバランサの背後に複数台のH2Oを設置する場合、Session Ticketをmemcachedで共有するように設定することが可能です
・ついでに、Memcached を用いた複数サーバにまたがるSession Resumptionにも対応しているので、Mobile Safari でのHTTPSアクセスも高速に処理することができます

というわけで、安全なウェブサイトを構築したい人は H2O 使えばいいんじゃないでしょうか。ちなみに、H2O 使うとユーザの体感速度も速くなるのでいいことづくめかと思います。

以上、ウェブサーバの設定に関する注意喚起、兼、H2Oの宣伝でした。

Thursday, June 18, 2015

H2OとPHPを組み合わせるの、超簡単です(もしくはmod_rewriteが不要な理由)

FastCGI対応機能がH2Oにマージされたことを受けて、uzullaさんが「H2OでPHP(がちょっとだけ動くまで)」という記事を書いてくださっています。

ありがたやありがたや。

その中で、
http://hoge/entry/1 みたいなのをphpにマップする方法はまだよくわかってません。その内しらべます

github.comを読む限り
FastCGI (or PHP) applications should be as easily configurable as it is for the Apache HTTP server
ということで、やったぜ!ってなるんですけど、nginxはもとより、Apacheにおいても現状ルーターをつかっているようなアプリだとhtaccessをいちいちかかないといけないので、Apacheみたいなスタイルが楽なのか?というとちょっと疑問があります。

(たとえば以下みたいなの)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
ここらへんがスッっとかけないと、まあApache、nginx同様にH2Oの設定のタレが必要になるかなあ、という感じはあります。
いやはや、まったくそのとおりなわけです。

ちなみにこの手のことをNginxでやろうとするともっと難しくて、以下のようにtry_filesやfastcgi_split_path_infoを使わないと脆弱性が発生するなんて話があるそうです(参照: Setting up PHP-FastCGI and nginx? Don’t trust the tutorials: check your configuration! » Neal Poole)。これ、みんなちゃんとできてるんですかね?
# Pass all .php files onto a php-fpm/php-fcgi server.
location ~ \.php$ {
   try_files $uri =404;

   fastcgi_split_path_info ^(.+\.php)(/.+)$;
   include fastcgi_params;
   fastcgi_index index.php;
   fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
   fastcgi_pass php;
}

こんな難しい設定、書けるわけないだろうが!!!!!!!

というわけで、H2Oの場合はもっと簡単に設定できるようになっています。以下のような感じ。
paths:
  "/":
    # /path/to/doc-root以下の静的ファイルを返す(存在した場合)
    file.dir: /path/to/doc-root
    # 存在しなければ、/index.php/ に内部転送
    redirect:                     
      url: /index.php/
      internal: YES
      status: 307

単純ですね。どうなってるかはコメントを見れば自明かとも思いますが、以下解説します。

実は、H2Oの設定ファイルにおいては、1つのパスに複数のハンドラを設定することができるのです。

複数のハンドラが設定されている場合、H2O は有効なレスポンスが生成できるまで、ハンドラを順次実行していきます。つまり、コメントにあるように、ファイルが存在すればそれを返すし、存在しなければ /index.php/ 以下にリクエストを内部的に転送し、その転送した結果の応答をクライアントに返すわけです。

わかりやすいですね。簡単ですね。


少し理屈っぽい話をすると、mod_rewriteのようなリクエストを書き換える黒魔術は、おおむね3種類の目的に使われてきました。第1の目的は、複数のローカルのディレクトリツリーを重ね合わせてひとつのhttpサイトとして表示すること。第2の目的は、uzullaさんが指摘したような、特定の条件をもつ以外のURLの処理を、FastCGIやアプリケーションサーバに委譲すること。第3の目的は、User-Agentや言語設定によって異なる応答を返すこと。

ですが、前2者を実現するためだけならば、正規表現を用いてURLを書き換えるような手法は過剰にすぎて管理がしづらかったわけです。H2OにおけるPHP対応においては、この点を意識し、複数のハンドラの連鎖を可能にすることで、設定が安全簡潔にできるようにしたのでした。

ちなみに、uzullaさんの例ではphp-fpmを使っていますが、H2Oはphp-cgiを自分で管理することができるので、小規模なウェブサイトにおいては、
file.custom-handler:          
  extension: .php
  fastcgi.spawn: "PHP_FCGI_CHILDREN=10 exec /usr/bin/php-cgi"
のように書くと、別途FastCGIデーモンを管理する必要がなくなって、より幸せになれるでしょう(名前はphp-cgiでも、この設定においてはFastCGIとして動作します)。


最後になりますが、ここで説明したFastCGI対応機能を盛り込んだH2Oバージョン1.3は今朝方リリースされたので、PHPerの皆様もそれ以外の皆様も、お試しいただければ幸甚に存じます。

リンク先のリリース告知エントリにも書いてあるけど、他のウェブサーバより表示速度(first-paint time)が、かなり速いですよ!

Thursday, May 14, 2015

jailing - chroot jailを構築・運用するためのスクリプトを書いた

個人サーバで外部に公開するサービスを動かすときには、chrootを使うにこしたことはないわけです。サービス毎にchrootしてあれば、サーバソフトウェアにセキュリティホールがあっても、他の情報が漏洩したりする可能性をぐっとおさえることができるわけですから。

でも、そのためだけにVPSにdockerとかコンテナを入れて使うってのは、構築も運用もめんどくさいし、ディスク容量食うし、やりたくない。systemd-nspawnも割と重たい雰囲気だし、LTSなubuntuだとそもそもsystemd入ってないし…

俺たちがほしいのは、ホストの環境の一部のみにアクセスできる、手軽なjailだー! ってわけで、ざっくり書いたのが、jailing。

github.com/kazuho/jailing

/usr/bin等、OS由来のディレクトリをchroot環境にread-onlyでエクスポートしつつ、指定されたコマンドを、そのchroot環境で動かすスクリプトです。

/usr/localや/homeといったディレクトリはエクスポートしないので、chroot環境下のソフトウェアにセキュリティホールがあって侵入されたとしても、(カーネルにバグがなければ)chroot環境外の情報が漏洩することはありません。

ホストとchroot環境でディレクトリを共有するためには、--bindオプションを使います。

たとえば、/usr/local/apache下にインストールしたApacheをchroot環境下で動かしたいなって時、jailingを使えば、以下のようにコマンド一発でchroot環境を作成して実行できます。
% sudo jailing --root=/var/httpd-jail \
    --bind /usr/local/apache \
    -- \
    /usr/local/apache/bin/httpd \
    -c /usr/local/apache/conf/httpd.conf
あるいは、/usr/local/h2o下にインストールしたH2Oをchroot環境下で動かす場合は、こんな感じ。
% sudo jailing --root /var/h2o-jail \
    --bind /usr/local/h2o \
    -- \
    /usr/local/h2o/bin/h2o \
    -m daemon \
    -c /usr/local/h2o/etc/h2o.conf
あるいは、jail内に入るには、
% sudo jailing --root /var/h2o-jail \
    -- \
    bash
とかやればいいわけです。

簡単ですね!

詳しくはman jailingしたりしてください。それでは〜

Monday, May 11, 2015

Clangに対応し、より高速になったqrintf version 0.9.2をリリースしました

ひさしぶりにqrintf関連の作業を行い、バージョン0.9.2をリリースしました。

ご存知のように、qrintfは、ccacheやdistccと同様の仕組みでCコンパイラのラッパーとして動作する、sprintf(とsnprintf)の最適化フィルターです。

qrintfを利用することで、整数や文字列をフォーマットするsprintfやsnprintfは最大10倍高速化され、また、H2Oのようなhttpdが20%程度高速化することが知られています。

今回のリリースは、0.9.1以降に行われた以下の改善を含んでいます。


以下の実行例からも、GCCでもClangでもIPv4アドレスの文字列化ベンチマークにおいて、qrintfを利用することで10倍以上の高速化が実現できていることが分かります。
$ qrintf --version
v0.9.2
$ gcc -O2 examples/ipv4addr.c && time ./a.out 1234567890
result: 73.150.2.210

real 0m2.512s
user 0m2.506s
sys 0m0.003s
$ qrintf gcc -O2 examples/ipv4addr.c && time ./a.out 1234567890
result: 73.150.2.210

real 0m0.173s
user 0m0.170s
sys 0m0.002s
$ clang -O2 examples/ipv4addr.c && time ./a.out 1234567890
result: 73.150.2.210

real 0m2.487s
user 0m2.479s
sys 0m0.004s
$ qrintf clang -O2 examples/ipv4addr.c && time ./a.out 1234567890
result: 73.150.2.210

real 0m0.220s
user 0m0.214s
sys 0m0.002s

今後は、qrintfをH2Oにバンドルすることで、qrintfの恩恵をより多くの利用者に届けて行きたいと考えています。同様のことは、他のソフトウェアプロジェクトでも可能なのではないでしょうか。

それでは、have fun!

Thursday, February 5, 2015

なぜ今、新しいHTTPサーバが必要なのか - H2O について勉強会で話したこと

先月末の話になりますが、SAPジャパンさんを会場に開催されたデータ転送ミドルウェア勉強会で、私が中心になって開発しているHTTPサーバ「H2O」について話す機会をいただき、登壇してきました。

以下は当日使用したスライドです。なぜ今H2Oを開発しているのか、その背景にある現状認識と将来の方針について、日本語で説明してあるので、興味ある方はご覧ください。


発表の機会をくださった@repeatedlyさんと@frsyukiさん、会場を提供してくださったSAPジャパンさん、ありがとうございました。

H2Oの開発は順調に進んでおり、HTTP/2サーバプッシュへの対応も完了し、まもなく次のバージョンがリリースできるかと思います。今後ともよろしくお願いいたします。

Monday, December 22, 2014

URL パーサにおける IPv6 対応

プログラマにとって IPv6 対応といえば、C言語のような低レベルな言語の話であって、ホスト名を文字列で扱うスクリプト言語には関係ないと考えがちです。ですが、実際には、文字列の取り扱いにおいても対応が必要になるところがあります。

その代表例が URL のパーサです。多くのエンジニアがイメージする URL の書式は「protocol://host:port/path#fragment」です。この書式を見れば、「:」と「/」を用いてトークンを分割するのが自然であり、RFC を参照せずに記述された URL パーサは、host の中に「:」が含まれないことを前提としていることがあります。あるいは、/[0-9A-Za-z_-\.]+/ のような正規表現を使って、ホスト名または IPv4 アドレスをチェックしている場合も多いのではないでしょうか。

ところが、IPv6 アドレスの文字列表現には「:」が含まれるため、IPv6 のアドレスを含む URL については、

http://[::FFFF:129.144.52.38]:80/index.html

のように、「[]」で囲むこと、と規定されています注1。つまり、「http://」のあとの最初の「:」をポート番号の始まりだと解釈する URL パーサは、IPv6 アドレスを正しく取り扱えないのです。

今後、IPv6 を利用する機会が増えるにともない、このような IPv6 アドレスを含んだ URL を取り扱えないパーサによるバグに直面する可能性も出てくるでしょう。

年末の大掃除がてら、自分が使っているURLパーサの挙動を確認・修正するのも良いかもしれませんね注2。

注1: 参照 RFC 3986
注2: なにを言いたかったというと、H2Oの URL パーサで IPv6 対応するのを忘れていたってことですね!!!!! というわけでこの記事は H2O Advent Calendar 2014 の一部です。てへぺろ

Tuesday, December 16, 2014

GitHub で submodule ではなく subtree を使うべき理由

GitHub には、タグを打つとソースパッケージを自動的にリリースするという機能があります。スクリプト言語においては、それぞれの言語について一般的なパッケージ管理システム注1があるため、この機能を使うことが少ないかと思いますが、デファクトのパッケージ管理システムが存在しないC等の言語で書かれたプログラムや、単独で動作する管理用のスクリプトを GitHub で開発・配布する際には、本機能はとても便利なものです。

しかし、この機能は git-archive コマンドのラッパーとして実装されているため、サブモジュールのファイルが含まれないという問題を抱えています。この点は GitHub の人たちも認識しているものの、今のところ GitHub で独自に対応するということは考えていないようです注2。

私がこの問題を 知ることになったのは、picojson の issue で指摘を受けたからです。picojson については問題が「テストが動かない」という程度なので後回しにしても良かったのですが、H2O についても同様の問題が発生することが目に見えていました。

そこでどうするか、irc で相談、実験した結果、サブモジュールのかわりに サブツリーを使えば、参照先のファイルについても git-archive の結果に含めることが可能であることがわかり、picojson についてはサブツリーへの移行を完了しました。

ツールの仕様に引っ張られてやり方を変えるという、ある意味しょうもない話なのですが、H2O についても今後リリースまでにサブツリーへの切り替えを行おうと考えています。

※本記事も H2O Advent Calendar 2014 の一部です。

注1: たとえば Perl については CPAN、JavaScript については NPM が存在する
注2: 参照: » Github zip doesn’t include Submodules Academic Technology Group Developers Blog のコメント

Saturday, December 13, 2014

C言語で可変長引数をとる関数を、型安全に書く方法

C言語の可変長引数は、型安全でない(まちがった型の引数を渡してもコンパイルエラーにならない)とされています。これは言語仕様の理解としては正しいのですが、特定の型の引数を任意の個数とる関数に限っては、マクロを使うことで型安全性を確保することができます。

任意の個数のdoubleを引数にとり、その和を返す関数「sumf」を例にあげて説明します。

C言語の可変長引数機構を使ってsumfを定義すると、以下のようになります。
#include <math.h>
#include <stdarg.h>
#include <stdio.h>

static double sumf(double nfirst, ...)
{
  double r = 0, n;
  va_list args;

  va_start(args, nfirst);
  for (n = nfirst; ! isnan(n); n = va_arg(args, double))
    r += n;
  va_end(args);

  return r;
}

int main(int argc, char **argv)
{
  printf("%f\n", sumf(NAN)); /* => 0 */
  printf("%f\n", sumf(1., NAN)); /* => 1 */
  printf("%f\n", sumf(1., 2.5, 3., NAN)); /* => 6.5 */
  return 0;
}
が、この定義には「NANを終端に使っているがために、NANを引数として渡すことができない(=終端を表す値が必要になる)」「型安全でない」という2点の問題があります。後者については、たとえば、sumf(1, 1, NAN)のように、うっかりdouble型以外の引数を渡してしまってもコンパイルエラーにならず、ただ結果がおかしくなったりコアダンプしたりすることになります注1。

では、どのようにsumfを定義すれば良いのでしょう。答えを書いてしまうと、こんな感じです。
#include <stdio.h>

#define sumf(...)                                       \
  _sumf(                                                \
    (double[]){ __VA_ARGS__ },                          \
    sizeof((double[]){ __VA_ARGS__ }) / sizeof(double)  \
  )

static double _sumf(double *list, size_t count)
{
  double r = 0;
  size_t i;

  for (i = 0; i != count; ++i)
    r += list[i];

  return r;
}

int main(int argc, char **argv)
{
  printf("%f\n", sumf()); /* => 0 */注2
  printf("%f\n", sumf(1.)); /* => 1 */
  printf("%f\n", sumf(1., 2.5, 3)); /* => 6.5 */
  return 0;
}
この定義では、可変長の引数群をマクロを用いてインラインで配列として初期化し、かつ、その要素数をsizeof演算子を用いて計算しています。そのため、C言語標準の可変長引数機構を使った場合の問題はいずれも発生しません。要素数が_sumf関数に引数countとして渡されるため、終端を表す特殊な値は必要になりませんし、また、実引数はdouble型の配列として呼出側で構築されるため、誤った型の引数を渡してしまうとコンパイルエラーになります。あるいは、たとえばint型の値を渡してしまった場合は、コンパイラによってdouble型に昇格することになるからです。

私たちが開発しているHTTPサーバ「H2O」では、この手法を用いて、型安全な文字列結合関数h2o_concatを定義、使用しています。

以上、H2Oで使っているC言語の小ネタ紹介でした。

※この記事はH2O Advent Calendar 2014の一部です。

注1: 手元の環境だと、sumf(1, 1, NAN)の結果は1となります
注2: 可変長マクロに対して0個の引数を渡すのはC99の規格には違反しますが、GCCやClangは問題なく処理します

Monday, December 8, 2014

64bit時代のバッファ処理

プログラミングの「常識」は時代とともに変化します。そのひとつが、サーバプログラムにおけるバッファ処理です。

1990年代後半から2010年頃までは、メモリ空間の大きさ(32bitすなわち4GB注1)を超える大きさのファイルを扱う時代でした。このため、httpdなどのサーバプログラムにおいても、入出力データをいったんテンポラリファイルとしてバッファリングする必要がありました。ですが、ファイルI/Oはメモリアクセスと比べると低速です。このため、小さなサイズのデータについてはメモリアクセスする一方で、大きなサイズのデータについてはファイルI/Oを用いる、という煩雑なコードを書く必要がありました。

しかし、2014年も暮れとなる今 、サーバサイドにおいては64bit環境のみを考えれば良い時代に入りつつあります。

もちろん、64bit環境といったところで、64bit空間の全てをユーザプロセスが使えるわけではありません。現行のx86-64 CPUがサポートする論理アドレス空間は48bit(256TB相当)であり、多くのOSではその上位アドレス半分がカーネル空間に割り当てられ、残った128TBがユーザプロセスで使用可能な空間となっています注2。

128TBものメモリ空間があれば、全てのテンポラリデータを「メモリ空間にマッピングして」使用することができます。

実際に、H2Oのバッファ処理では、64MBを超えるものについてはテンポラリファイルをftruncate/mmapすることで領域を確保注3し、これに対してメモリアクセスを行うようになっています。詳しくはh2o_buffer_reserve関数の定義をご覧ください。

バッファを利用する側のコードにおいては、サイズの大小に関係なく単なるメモリブロックとしてアクセスできるため、コードが単純かつ高速になるのです。

※本記事はH2O Advent Calendar 2014の一部です。

注1: 多くのOSにおいては、カーネルとメモリ空間を分け合う必要があるため、実際にユーザプロセスで使用できるメモリ空間は1GB〜3GB程度でした
注2: 参照: x86-64 - Wikipedia, the free encyclopedia - Operating system compatibility and characteristics
注3: malloc(3)で確保可能なメモリの総量である物理メモリ+スワップ領域の和は必ずしも十分に大きくないため、テンポラリファイルを生成してftruncateすることでディスク上に必要な領域を確保しています