文字列switchの性能分析
私たちがたくさんの命令を持っていると仮定します。本論文で述べられるように簡単にするために、これらの命令を全部一つの種類の方法に実現します。文字列名で対応するコマンドを呼び出すことができます。メソッド呼び出しは、大文字と小文字に敏感ではない。このコマンドクラスはこう見えます。
まず、小さいテストをしましょう。これらのコマンドを以下の方法で呼び出すと、 EqualsIgnoreCaller.call(obj,「Command 9」,arg); EqualsIgnoreCaller.call(obj,「Command 99」,arg); EqualsIgnoreCaller.call(obj,「Commmad 100」,arg); 私達はJMHの基準テストを書きに来ました。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase 9
thrpt
10
998445.66
3518.385
ops/s
t.ClalTest.testEquals IgnoreCase 99
thrpt
10
83494.967
1237.927
ops/s
t.ClalTest.testEquals IgnoreCase 100
thrpt
10
3701991.457
21054.106
ops/s
Command 100の速度は一番速くて、次はCommand 9で、最後はCommand 99です。なぜチェーンの最後のCommund 100は前のCommand 99より44倍早いですか?彼らの性能は同じではないですか?
もちろんです。String.equalsIgnoreCaseのソースコードを見に来ました。
Command 99はマッチを見つける前に89*9文字を比較する必要があります。次の二つの最適化があります。文字列と自分自身を比較すれば、初めての比較はそのまま通過します。この最適化はここで有用であり、すべての文字列は字面量であるため、メモリプールに存在します。この最適化はCommand 100にとってより重要です。全体の過程で文字列の内容を比較する必要はありません。
String.equalsにも同様のロジックがあります。まず唯一の検査で、長さ検査で、最後に内容の比較です。
このような命令代理の論理を実現するには、このような一連のequalsIgnoreCaseを使って呼び出すのが一番いいですか?もっといい方法がありますか?
文字列switchの大きさは敏感ではありません。
String.equalsIgnoreCaseの呼び出しチェーン
これは最も直接的な方法です。残念なことに、固定長のコマンドが一つだけあって、コマンド自体の長さが十分に短い場合にのみ、性能は計算されます。
命令名を小文字に変換して小文字のコマンド名と比較することができます。この方法はコマンド数が増える時によく表現されます。
Java 7からswitch文に文字列が使用されます。論理的には前のやり方と同じですが、実装上は違います。文字列switchはマッピングテーブルで実現され、文字列を対応する処理コードブロックにマッピングします。
Java 8では匿名クラスをlambada表現に置き換えることができます。
上記のクラスはGenerator.javaによって生成されます。ソースは本文の最後にあります。命令の名前と数量を修正してテストを完成します。
極端な場合――他のコマンドとは長さが違います。
上記の例では、これらのアルゴリズムの実行効率はいかがですか?100のコマンド:Commmad 1からCommmad 100までです。Commmad 100のアクセス時間を確認します。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
372262.950
96314.315
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
139947.42
11651.113
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2640137.15
17767.132
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
2673449.940
13438.176
ops/s
t.ClalTest.testSwitch Call
thrpt
10
2653356.312
22085.321
ops/s
equalsIgnoreCaseは一番速いです。equalsは一番遅いです。コードはほとんど同じです。一箇所のequals用例を除いて、それは自動的にtolowerCase方法を呼び出されます。これは違います。switchとmapのテスト結果は同じです。
100個の同じ長さのコマンド:
テストセットを修正します。100コマンドをCommmad 1001からCommmad 10100に変更します。コマンド長が同じシーンが多くテストされます。私たちがテストしたのはCommmad 10100のアクセス時間です。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
62066.170
156.87
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
450102.165
1121.56
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2411420.37
11454.230
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
2409035.80
54165.643
ops/s
t.ClalTest.testSwitch Call
thrpt
10
2406538.563
10945.766
ops/s
500個の同じ長さのコマンド
私たちは500個のコマンドを試してみました。Commund 1001からCommund 10100までのアクセス時間をテストしました。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
12899.27
886.002
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
49137.448
976.380
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2435168.60
28192.40
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
2488337.117
170484.548
ops/s
t.ClalTest.testSwitch Call
thrpt
10
948982.350
2652.893
ops/s
同じ長さのコマンド1000個
最後に1000個のコマンドを試してみました。Commund 11からCommmad 1000までです。テストはCommmad 1000のアクセス時間です。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
4338.513
153.786
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
7602.722
746.926
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2147853.17
45741.062
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
239853.845
10194.687
ops/s
t.ClalTest.testSwitch Call
thrpt
10
896876.772
5740.585
ops/s
これらの3つのテストでは、mapのアクセス時間はほとんどすべてのテストケースでほぼ同じであり、equals/equals IgnoreCaseはコマンド数の増加とともに大きく変化することが見られます。
文字列switchの実現
文字列switchの性能は非常に興味深いです。対数速度の低下を示しています。これはswitchが固定サイズのmapとして実現されたことを意味します。この固定されたMAPの大きさはどれぐらいかを探してみます。ここで少なくとも2つの操作の性能を計算する必要があります。command Name.toLowercase()とcommman d 1.equals(command 2)。JMH 2)
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testLC
thrpt
10
3262501.38
61071.448
ops/s
t.ClalTest.testEQ
thrpt
10
53070517.985
232819.742
ops/s
switchのテストには、command Name.toLowerCase()が含まれます。後は大量のequals呼び出しがあります。そのため、次の式を通じてequalsメソッドの呼び出し回数を計算できます。
TEST TIME=LC TIME+N*EQuTIME
N=(TESTUTIME-LCuTIME)/EQUTIME
私のテストでは、タイミングが少し違っています。最終的には100個のコマンドが5つのequals呼出に対応しています。1000個のコマンドは約50個のequals呼出があります。これはswitchのmapテーブルの中に20個のビットがあることを意味します。
結論 Java 7中の文字列switchは固定サイズのMAPを使用して実現される。これは大多数の場合、自由に使用できることを意味し、性能の問題はあまり心配しなくてもいい。見たように、比較したcase数が100未満の場合、文字列switchの性能は手動でMAPを実現するのと同じである。 もしあなたのcase文の文字列の長さが違っていて、それほど大きくないなら、String.equals/equals IgnoreCaseはswitchの性能よりいいです。これはString.equals/equalsIgnoreCaseの中で文字列の長さを先に比較してから実際の内容を比較するからです。
ソースコード
オリジナルの文章の転載は出典を明記してください。
http://it.deepinmind.com
英語のテキストリンク
public class ObjectWithCommands {
public Object Command1( final Object arg ) { return arg; }
public Object Command2( final Object arg ) { return arg; }
...
public Object Command9( final Object arg ) { return arg; }
public Object Command10( final Object arg ) { return arg; }
...
public Object Command99( final Object arg ) { return arg; }
public Object Command100( final Object arg ) { return arg; }
}
本明細書では、これらのコマンドを呼び出す異なる方法間の性能の違いを比較する。まず、小さいテストをしましょう。これらのコマンドを以下の方法で呼び出すと、
public class EqualsIgnoreCaseCaller {
public static Object call( final ObjectWithCommands obj, final String commandName, final Object arg )
{
if ( commandName.equalsIgnoreCase( "Command1" ) )
return obj.Command1( arg );
if ( commandName.equalsIgnoreCase( "Command2" ) )
return obj.Command2( arg );
...
if ( commandName.equalsIgnoreCase( "Command99" ) )
return obj.Command99( arg );
if ( commandName.equalsIgnoreCase( "Command100" ) )
return obj.Command100( arg );
}
}
次のどの方法でコールが一番早いですか?
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS )
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS )
@Threads(1)
@State(Scope.Thread)
public class CallTest {
private String m_commandName9 = "Command9";
private String m_commandName99 = "Command99";
private String m_commandName100 = "Command100";
private ObjectWithCommands m_obj = new ObjectWithCommands();
private Object m_arg = new Object();
@GenerateMicroBenchmark
public Object testEqualsIgnoreCase9()
{
return EqualsIgnoreCaseCaller.call(m_obj, m_commandName9, m_arg);
}
@GenerateMicroBenchmark
public Object testEqualsIgnoreCase99()
{
return EqualsIgnoreCaseCaller.call(m_obj, m_commandName99, m_arg);
}
@GenerateMicroBenchmark
public Object testEqualsIgnoreCase100()
{
return EqualsIgnoreCaseCaller.call(m_obj, m_commandName100, m_arg);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(".*" + CallTest.class.getSimpleName() + ".*")
.forks(1)
.build();
new Runner(opt).run();
}
}
以下は私のノートでの運行結果です。Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase 9
thrpt
10
998445.66
3518.385
ops/s
t.ClalTest.testEquals IgnoreCase 99
thrpt
10
83494.967
1237.927
ops/s
t.ClalTest.testEquals IgnoreCase 100
thrpt
10
3701991.457
21054.106
ops/s
Command 100の速度は一番速くて、次はCommand 9で、最後はCommand 99です。なぜチェーンの最後のCommund 100は前のCommand 99より44倍早いですか?彼らの性能は同じではないですか?
もちろんです。String.equalsIgnoreCaseのソースコードを見に来ました。
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
ここで最も主要な最適化は文字列の長さが等しいかどうかを比較することである。この検査により、Commund 100は前の99個よりずっと速くなります。それによってスピードが一番速いものになります。この最適化はCommand 99にとっては前の9回の比較をスキップするしかないです。Command 99はマッチを見つける前に89*9文字を比較する必要があります。次の二つの最適化があります。文字列と自分自身を比較すれば、初めての比較はそのまま通過します。この最適化はここで有用であり、すべての文字列は字面量であるため、メモリプールに存在します。この最適化はCommand 100にとってより重要です。全体の過程で文字列の内容を比較する必要はありません。
String.equalsにも同様のロジックがあります。まず唯一の検査で、長さ検査で、最後に内容の比較です。
このような命令代理の論理を実現するには、このような一連のequalsIgnoreCaseを使って呼び出すのが一番いいですか?もっといい方法がありますか?
文字列switchの大きさは敏感ではありません。
String.equalsIgnoreCaseの呼び出しチェーン
これは最も直接的な方法です。残念なことに、固定長のコマンドが一つだけあって、コマンド自体の長さが十分に短い場合にのみ、性能は計算されます。
if ( commandName.equalsIgnoreCase( "Command1" ) )
return obj.Command1( arg );
if ( commandName.equalsIgnoreCase( "Command2" ) )
return obj.Command2( arg );
...
Command.toLowerCaseその後String.equals命令名を小文字に変換して小文字のコマンド名と比較することができます。この方法はコマンド数が増える時によく表現されます。
final String lcName = commandName.toLowerCase();
if ( lcName.equals( "command1" ) )
return obj.Command1( arg );
if ( lcName.equals( "command2" ) )
return obj.Command2( arg );
Java 7の文字列switchJava 7からswitch文に文字列が使用されます。論理的には前のやり方と同じですが、実装上は違います。文字列switchはマッピングテーブルで実現され、文字列を対応する処理コードブロックにマッピングします。
final String lcName = commandName.toLowerCase();
switch( lcName ) {
case "command1":
return obj.Command1( arg );
case "command2":
return obj.Command2( arg );
...
}
文字列switchと明示的な小文字コマンド名を使用してコマンドに向けたmapを比較します。各コマンドは匿名クラスで実現されます。
interface ICommandCaller {
public Object call( final ObjectWithCommands obj, final Object arg );
}
Java 8の前に実現されるのは、このようなものである。
private static final Map<String, ICommandCaller> CALL_MAP = new HashMap<>( 100 );
static {
CALL_MAP.put( "command1", new ICommandCaller() {
public Object call( final ObjectWithCommands obj, final Object arg ) {
return obj.Command1( arg );
}
} );
CALL_MAP.put( "command2", new ICommandCaller() {
public Object call( final ObjectWithCommands obj, final Object arg ) {
return obj.Command2( arg );
}
} );
...
}
public static Object call( final ObjectWithCommands obj, final String commandName, final Object arg )
{
return CALL_MAP.get( commandName.toLowerCase() ).call( obj, arg );
}
Java 8のlamdaJava 8では匿名クラスをlambada表現に置き換えることができます。
private static final Map<String, ICommandCaller> CALL_MAP = new HashMap<>( 100 );
static {
CALL_MAP.put( "command1", ( obj, arg ) -> obj.Command1( arg ) );
CALL_MAP.put( "command2", ( obj, arg ) -> obj.Command2( arg ) );
...
}
テスト上記のクラスはGenerator.javaによって生成されます。ソースは本文の最後にあります。命令の名前と数量を修正してテストを完成します。
極端な場合――他のコマンドとは長さが違います。
上記の例では、これらのアルゴリズムの実行効率はいかがですか?100のコマンド:Commmad 1からCommmad 100までです。Commmad 100のアクセス時間を確認します。
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS )
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS )
@Threads(1)
@State(Scope.Thread)
public class CallTest {
private String m_commandName = "Command100";
private ObjectWithCommands m_obj = new ObjectWithCommands();
private Object m_arg = new Object();
@GenerateMicroBenchmark
public Object testEqualsIgnoreCase()
{
return EqualsIgnoreCaseCaller.call(m_obj, m_commandName, m_arg);
}
@GenerateMicroBenchmark
public Object testEqualsLowerCase()
{
return EqualsCaller.call(m_obj, m_commandName, m_arg);
}
@GenerateMicroBenchmark
public Object testSwitchCall()
{
return SwitchCaller.call(m_obj, m_commandName, m_arg);
}
@GenerateMicroBenchmark
public Object testJava7Map()
{
return Map7Caller.call(m_obj, m_commandName, m_arg);
}
@GenerateMicroBenchmark
public Object testJava8Map()
{
return Map8Caller.call(m_obj, m_commandName, m_arg);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(".*" + CallTest.class.getSimpleName() + ".*")
.forks(1)
.build();
new Runner(opt).run();
}
}
以下はテスト結果です。Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
372262.950
96314.315
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
139947.42
11651.113
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2640137.15
17767.132
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
2673449.940
13438.176
ops/s
t.ClalTest.testSwitch Call
thrpt
10
2653356.312
22085.321
ops/s
equalsIgnoreCaseは一番速いです。equalsは一番遅いです。コードはほとんど同じです。一箇所のequals用例を除いて、それは自動的にtolowerCase方法を呼び出されます。これは違います。switchとmapのテスト結果は同じです。
100個の同じ長さのコマンド:
テストセットを修正します。100コマンドをCommmad 1001からCommmad 10100に変更します。コマンド長が同じシーンが多くテストされます。私たちがテストしたのはCommmad 10100のアクセス時間です。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
62066.170
156.87
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
450102.165
1121.56
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2411420.37
11454.230
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
2409035.80
54165.643
ops/s
t.ClalTest.testSwitch Call
thrpt
10
2406538.563
10945.766
ops/s
500個の同じ長さのコマンド
私たちは500個のコマンドを試してみました。Commund 1001からCommund 10100までのアクセス時間をテストしました。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
12899.27
886.002
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
49137.448
976.380
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2435168.60
28192.40
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
2488337.117
170484.548
ops/s
t.ClalTest.testSwitch Call
thrpt
10
948982.350
2652.893
ops/s
同じ長さのコマンド1000個
最後に1000個のコマンドを試してみました。Commund 11からCommmad 1000までです。テストはCommmad 1000のアクセス時間です。
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testEquals IgnoreCase
thrpt
10
4338.513
153.786
ops/s
t.ClalTest.testEquals LowerCase
thrpt
10
7602.722
746.926
ops/s
t.ClalTest.testJava 7 Map
thrpt
10
2147853.17
45741.062
ops/s
t.ClalTest.testJava 8 Map
thrpt
10
239853.845
10194.687
ops/s
t.ClalTest.testSwitch Call
thrpt
10
896876.772
5740.585
ops/s
これらの3つのテストでは、mapのアクセス時間はほとんどすべてのテストケースでほぼ同じであり、equals/equals IgnoreCaseはコマンド数の増加とともに大きく変化することが見られます。
文字列switchの実現
文字列switchの性能は非常に興味深いです。対数速度の低下を示しています。これはswitchが固定サイズのmapとして実現されたことを意味します。この固定されたMAPの大きさはどれぐらいかを探してみます。ここで少なくとも2つの操作の性能を計算する必要があります。command Name.toLowercase()とcommman d 1.equals(command 2)。JMH 2)
private String m_commandName = "Command10500";
private String m_commandName2 = "Command10090";
@GenerateMicroBenchmark
public String testLC()
{
return m_commandName.toLowerCase();
}
@GenerateMicroBenchmark
public boolean testEQ()
{
return m_commandName.equals( m_commandName2 );
}
Benchmark
Mode
Samples
Mean
Mean error
ユニティ
t.ClalTest.testLC
thrpt
10
3262501.38
61071.448
ops/s
t.ClalTest.testEQ
thrpt
10
53070517.985
232819.742
ops/s
switchのテストには、command Name.toLowerCase()が含まれます。後は大量のequals呼び出しがあります。そのため、次の式を通じてequalsメソッドの呼び出し回数を計算できます。
TEST TIME=LC TIME+N*EQuTIME
N=(TESTUTIME-LCuTIME)/EQUTIME
私のテストでは、タイミングが少し違っています。最終的には100個のコマンドが5つのequals呼出に対応しています。1000個のコマンドは約50個のequals呼出があります。これはswitchのmapテーブルの中に20個のビットがあることを意味します。
結論
ソースコード
オリジナルの文章の転載は出典を明記してください。
http://it.deepinmind.com
英語のテキストリンク