これは seekable な stream と none_seekable な stream の使い分けに関する記事です。

使い分けが十分出来ている人は読まなくても大丈夫です。

皆さんは bitstream という単語をご存知でしょうか?

AV (Audio&Visual) が好きな人や、 それらの業界に関係のある人ならそこそこ聞く単語だと思いますが、 一般的にはあまり馴染の無い単語でしょうか。

馴染の無い人の為に身近な HDD レコーダを例に挙げて説明すると、 HDD レコーダはデジタル放送の電波に乗っているデータをそのまま記録していますが、 このデータが bitstream です。 HDD レコーダは、デジタル放送の bitstream を HDD に記録し、 記録した bitstream を再生する装置と言えます。 もちろん、実際にはそんな単純ではないですが、概ね間違ったことは言ってません。

stream

プログラムでデータを扱う時、stream という概念を使って制御します。

言語stream (入力)
JavaInputStream
SwiftInputStream
Goio.Reader

上記は言語毎の入力系 stream の例です。

ちなみに入力系の stream とは何かというと、 流れてくるデータを読み出すためのものです。

例えば、先ほどの HDD レコーダの例で説明すると、

  • デジタル放送の電波に乗っている bitstream を読み取る部分
  • HDD に記録されている bitstream を読み込む部分

が入力系の stream です。

また、上記言語の stream (InputStream,io.Reader)には共通することがあります。

それは、データの流れが一方通行で遡ることが出来ない、ということです。

プログラム的に言うと、上記の stream は seek や rewind をサポートしていません。

これを、先ほどの HDD レコーダの例で説明すると、 「過去に放送された番組の録画はできない」ということです。

24 時間全ての番組を常に録画し続けて、 「1週間前に放送された任意の番組を再生する」機能を持つ HDD レコーダはありますが、 それはあくまで録画してあるものを再生しているのであって、 過去に放送された番組を録画することは出来ません。 もしそれが出来るなら、 本当の意味でのタイムマシーンを作ることが出来ることと同義になります。

なお、「過去に放送された番組の録画はできない」ですが、 「録画した番組」の逆再生などは出来ます。

先ほど説明した通り、次のどちらもの入力 stream です。

  • デジタル放送の電波に乗っている bitstream を読み取る部分
    • 過去に放送された番組の録画はできない
  • HDD に記録されている bitstream を読み込む部分
    • 録画した番組は逆再生など出来る

これはつまり、 stream には次の 2 つのタイプが存在することを意味します。

  • 流れが一方通行で遡ることが出来ない stream
  • 流れを遡ることが出来る stream

これ以降、上記をそれぞれ none_seekable と seekable とします。

none_seekable と seekable の使い分け

上記の通り、stream には none_seekable と seekable の 2 つのタイプが存在します。

では、実際のプログラムでは stream はどう使い分けるべきか? と考えた場合、 seekable である必要がない場合は極力 none_seekable を使うべきです。

なぜならば、 seekable は none_seekable を包括する概念であり、 seekable な stream は none_seekable として使用することが出来ますが、 none_seekable な stream は seekable として使用することが出来ないからです。

次に、疑似言語を使って説明します。

fn funcA( data: seekable ) {
  sub( data );
}
fn funcB( data: none_seekable ) {
  sub( data );
}

上記は、 seekable な引数を持つ関数 funcA と、 none_seekable な引数を持つ関数 funcB を定義する疑似言語コードです。 また sub() は、 none_seekable な引数を持つ関数とします。

ここで、この関数 funcA は seekable な stream でしか使用できないのに対し、 この関数 funcB は seekable, none_seekable どちらでも使用できることになり、 funcB は funcA よりも汎用性が高いと言えます。

関数の汎用性が高いことが良いプログラムである、とは一概には言えませんが、 ミドルウェアなどのライブラリでは、汎用性が高い方が良いとされます。

つまり、 stream を入力に持つ関数の処理においては、 seek や rewind の使用は極力避け、 none_seekable の stream で処理可能にすべきである、と言えます。

ただし例外として、 seek や rewind を使用しないと目標のパフォーマンスが出ないとか、 必要なワークメモリが規定を越えてしまう、等の問題がある場合は、 無理に none_seekable で処理する必要はありません。

とはいえ、あくまでも原則は、 seekable ではなく none_seekable で処理できるかどうかを検討するべきです。

言語の組込みの型として seekable と none_seekable が分かれていない言語は、 結構あると思います。

そのような言語でも、 seekable と none_seekable の考え方自体は有効なので実践してください。

none_seekable で処理することのメリット

seekable ではなく none_seekable で処理することのメリットとして、 Web ブラウザでの処理を例に挙げて説明します。

もしもブラウザの処理が全て seekable であった場合、 ブラウジングスピードが遅くなることが予想されます。

なぜなら、Web ブラウザは、サーバから HTML をダウンロードし、 HTML 内のリンクを抽出し、そのリンクをさらにダウンロードします。 そしてリンクが画像の場合、画像をデコードして表示します。

画像のデコード処理が none_seekable であるならば、 画像データのダウンロード開始と同時にデコード処理が開始でき、 画像データのダウンロード終了とほぼ同時にデコード処理を完了できます。

一方でもしも画像のデコード処理が seekable だった場合、 画像データをダウンロード終了してからデコード処理を行なわなければならず、 その分タイムロスになります。 さらに欠点はタイムロスだけでなく、 画像データの全てをダウンロードして一旦 RAM やストレージに格納しておく必要があり、 その分のリソースを消費することになります。

画像データのサイズなんてイマドキのハードウェアスペックなら無視できる、 という意見もあるかもしれませんが、例えば 8K の低圧縮画像などは軽く数 10MB を越えます。 こういった画像のデータを全てダウンロードしてからデコードするなんてしてたら、 無駄にリソースを消費することが分かると思います。

また、最近はほとんど使われていませんが、 progressive JPEG なんて画像フォーマットが使われていた時期がありましたが、 これは none_seekable で処理して始めて意味のあるものです。

progressive JPEG を簡単に説明すると、 画像データの一部をダウンロードするだけで、低解像度の画像をデコードできる技術で、 ダウンロードが進むごとにデコード結果の解像度が上がるというものです。

これは、ネットワークの通信速度が低速なころに使用されていた画像フォーマットで、 いまではほとんど使われなくなったものですが、 none_seekable で処理しなければ全く意味のないものです。

他にも none_seekable で処理することのメリットとして、 動画配信に代表されるストリーミングサービスがあります。

あれも、 none_seekable が前提にあるからこそ可能なサービスです。

「ストリーミングサービスが none_seekable だ」と書くと 「Youtube はシークできるぞ」とかツッコミがあると思うので一応補足しておきます。

たしかに Youtube などの動画配信サービスはシークできるのが当たり前です。 しかし、通常再生時は none_seekable で処理していて、 シークなどの操作が入った時だけ、 サーバからデータをダウンロードしなおして処理しています。 つまり、基本は none_seekable です。

もしも動画データが seekable 前提だった場合、 動画データを全てダウンロードしてからでないと再生できないか、 seek 処理が大量に発生してサーバ間の通信負荷が非常に高くなることが予想されます。

また、seekable(randam access) は none_seekable(sequential) と比べて 非常にパフォーマンスが悪くなるのが一般的です。

例えば HDD の randam access は sequential と比べて 2 桁以上のパフォーマンス劣化、 SSD でも 1 桁以上劣化します。 RAM であっても、randam access することでキャッシュミスが発生しやすくなり、 パフォーマンス劣化からは逃れられません。 現代ではほとんど使われませんが、 テープデバイスなんて使った日には、どれほどかかるか想像すら出来ません。

データフォーマット

stream を処理する際に、 それを none_seekable として扱うには、 stream に流れるデータのフォーマットが none_seekable として 扱い易い構造になっている必要があります。

データフォーマットが none_seekable として扱い難い構造の場合、 上記のように「目標のパフォーマンスが出ない」、「必要なワークメモリが規定を越えてしまう」 という問題が発生する可能性があります。

ある程度の大きさになるデータフォーマットを定義する時は、 必ず none_seekable で処理することを考えて定義しましょう。

なお、 stream で処理することが多い画像や音声などのデータフォーマットは、 基本的には none_seekable で処理できるように定義されています。

もしもそうでなければ、放送や動画配信でデジタルデータを扱うことは出来ません。

ちなみに、データの encode と decode の none_seekable での扱い易さは、 相反することがあります。

その場合、どちらかを優先するか、折衷案の検討が必要です。 一つ言えることは、作業バッファを 0 にすることはまず不可能なので、 どの程度の作業バッファサイズなら妥当かを判断することが重要です。

例外

none_seekable で処理することで、 ダウンロードとデコードを同時に処理できるため高速に処理できる、と説明しましたが、 一部例外があります。

それは、専用ハードウェアを使用してデコードする場合です。

HDD レコーダなどの家電製品では、 動画や音声を処理する専用ハードウェアを搭載しています。 それら専用ハードウェアは、データを渡すと高速に処理して結果を返してくれますが、 処理するデータは全て揃えてから渡さなければならない、 という制約があることがほとんどです。

その場合は、none_seekable でダウンロードとデコードを同時に処理するよりも、 専用ハードウェアを使用して処理する方が高速に処理できます。

ただし、当然専用ハードウェアであるため、処理できるデータは限られていますし、 そのような専用ハードウェアが利用できる環境は限られています。

まとめ

stream を扱う際は、次を注意する必要があります。

  • 極力 none_seekable で扱う
  • データフォーマットを決める時点で、 none_seekable で扱えることを考慮する

最後に

なんでこんなことを書いたかというと、 最近とある画像コーデックのライブラリを扱うことがあったんですが、 そのライブラリへの入力が seekable であることを前提としていてムカついた、 という経験をしたためです。

データ streaming 処理を行なう場合の基本的な考えなので、 必ずこれらを考慮に入れて設計するようにお願いします。

以上。