文字列連結について検証しようと思ったらそもそも検証の仕方が予想していたのと全然違った件

本記事は Java Advent Calendar 2015 19日目 です。19日目大変ギリギリの投稿で本当に申し訳ないです。

どうもこんにちは。まーやです。Javaの猛者が集うアドベントカレンダーになぜかうっかり参加しちゃった初心者系の人間のお話です。

最近お仕事で参画した某改修プロジェクトでは、文字列の連結の処理がものすごい多いソースでした。改修にあたり「そういえば文字の連結ってどのくらいの量の時にどれ使えばいいのかしらん…」とふと考えました。多分多くのできるエンジニアとっくにそんなところは通り過ぎたと思うし、ググれば”これくらい差があるよ!”って結果が出された記事もたくさん見ることがでるんですが、あえて「自分でも検証してみよう〜」というのが今回の記事の内容・・・だったんですが、タイトルの通り、脱線してまーやがいかに無知だったかの話になります。

【最初に考えていたこと】

  • 文字列の連結に使う処理として「str1 + str2」「concat」「StringBuffer」「StringBuilder」の4パターンを対象とする
  • それぞれ規定の回数分for文で連結する処理をつくる
  • IDEから実行して時間を計測

これをJava先生な方に伝えたところ「バカなの?」と一蹴された。しょぼん。

【知らなかったこと1〜検証時のウォームアップ〜】

9000回の同処理実施(メソッド単位)を境に、JITコンパイラが実行されるそうです。今まで単語だけなんとなく知っていただけで、実際のところ何する処理なんだか全く理解していませんでした。。。(やっとちょっと調べただけなので、まだまだ理解不足ではありますが。)今回は1万回くらいの連結と10万回くらいの実行を想定していたの1回の検証の中にJITな実行とそうでない実行が入り乱れることになります。そのため正しい値が取れないというか、実行するたびに時間がエラい変わってしまう、という事態になってしまいます。

そこで必要なのが「ウォームアップ」という事前処理。要は先にJITじゃない実行をさせた後に、計測を開始することで計測値のばらつきを抑えよう、少しでも正確な値にしよう、というものです。今回は以下の方法でウォームアップすることにしました。
「計測したい処理を1万回実行する」
この処理実行後、実際に計測を開始します。

【知らなかったこと2〜検証には失敗がある〜】

検証実験は同じソースで同じようにrunしても失敗と成功がある。理科の実験とか思い出せば確かにそうだよなぁという感じなんですが、「同じソースを通る時は同じ実行結果が得られる」ということを盲信していた私にとっては目から鱗。

今回指摘を受けたのはこちらのサイトにある内容。動的コンパイラと静的コンパイラの問題です。詳しいことは私の曖昧な認識かつ拙い日本語で書くよりサイト見ていただいた方がよっぽどよいので・・・ごらんいただければ幸いです。。。

そんなわけで今回は実行時のオプションに
「-XX:+PrintCompilation」
というオプションをつけて実行し、実際に文字連結処理が実行される前にコンパイルが完了していることを確認します。文字連結処理の後にコンパイルが行われてたら検証結果としてダメな子です。

検証良い子(例)
スクリーンショット 2015-12-19 21.34.19.png

検証悪い子(例)
スクリーンショット 2015-12-19 21.36.20.png

ちょっとわかりにくいんですが、「X ms」と書かれているのが実行時間の表示(つまり検証コード終了)部分です。

【とりあえずこのへんで一回検証してみよう】

まだまだ検証は奥が深いのは重々承知なんですが、とりあえず一回この辺で検証作業をしてみることにします。ソースコードはこんな感じになりました。リンク先にあるソースは「str1 + str2」のものです。こんな感じでfor文の中をそれぞれの処理に書き換えてそれぞれの処理を実行してみます。分析界隈の方から怒られそうですが、実行回数はとりあえず5回ずつで。。。一応Java SE 1.7 と Java SE 1.8 の2つでやってみました。

スクリーンショット 2015-12-19 21.42.50.png

実行結果の精度はまだまだかもしれませんが当初の目的だった「どれで連結すればいいの?」という疑問は一応解消できそうです。

やはり文字の連結回数が多ければ多いほど、StringBuilderやStringBufferを利用する方が良さそうです。1万回・10万回とかになるとそのスピードは顕著ですね。例えばプログラムの中で1回に連結する文字数が少なくても、全体として文字を連結することが多いのであれば、こまめにStringBuilder/StringBufferを利用するのが良さそうです。

とはいえ、例えば「3つ4つの文字連結したいだけなのに!」というときは単純に「str1 + str2」でも全然良いのではないのかなというのが個人的感想です(盲信的にStringBuilderをつかえ!という結果ではないよということです)。ちょっときになる方であればそういうときは「str1.concat(str2)」してあげればよいと思います。

・・・まぁすでにいろんなサイトで言われていることと同じ結果ですね。

【そのほかわかったこと】

数値の妥当性はイマイチ感があるので(いかんせん実行のサンプルが少ないし検証方法もまだいびつなので)「どれくらい」ということは言えませんが、やはりJava SE 1.7からJava SE 1.8の方が処理が効率的になった・・・のかな。

【今後引き続きやってみたいこと】

  • 検証のサンプルを多くしてもう少し精度あげてみたい
  • 検証方法間違ってるよとか、こうした方がいいよみたいなことがあったらみなさんからご教示いただきたい
  • 上記結果で「str1 + str2」の処理の場合、Java SE1.8よりJava SE 1.7の方が早い、みたいな結果になっているんですが、これってほんとなの?というところを調べてみたい

【追記】2015/12/20
本ブログの投稿をした後、@cero_tさんと@bitter_foxに色々ご教示いただいたのでこちらに追記しておきます。

ベンチマークツールのJMHというものがあるそうです。記事がとっても詳細まで書かれているので、大変ありがたい感じ。今回やりたかったこととかが簡単にできる&きちんとできるようなので、近々私もこちらを試してみたいと思います。

「str1 + str2」の実態はStringBuilderなので、↑のような結果になりますよ、というお話でした。

なんとJDK9からは一行で「str1 + str2 + str3…」と記述していく場合はStringBuilderを使うよりも高速になるらしい!が、ループを使った連結などの場合は引き続きStringBuilderのほうが以前早いかもしれないです、とのこと。リリースされたら要チェックですね!

「str1 + str2」の結果がJava SE 1.7の方が早いような結果になってしまった件について、@bitter_foxさんが検証してくださいました。ありがたや…!原因はウォームアップ不足だったとのこと・・・。

確かに出力された内容の最後のところ(計測がコンパイル後に完了しているか)しか確認していなかった。。。こちらも意識してやってみたいと思います。

【まとめ】

お気軽に始めたことが奥深くて泣きそうになりました。が、厳しいお言葉いただきながらJava先生にはお世話になりました。「知らないということを知る」ということの大事さを改めて痛感しました。生きることに傲慢にならず無知の知を得たいと心から思いました。

ということで近々久しぶりにソクラテスにハマろうと思います。ちょっと大学時代へタイムスリップ。

以上です。明日のアドベントカレンダーはnabedgeさんです。おたのしみに〜

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください