2018年4月18日水曜日

C#素人が陥ったハズカシイ失敗

ひとつ前の記事でご紹介したjavascriptの生成元はC#なのですが、それを作成する際にかいた恥を2つご紹介します。
まあ、そりゃそうだよね、というものばかりです(以下の記事で使ったデータを出したプログラムは文末に付録としてご覧いただけます)。

1.インスタンスの生成時間が異常に遅い
例えば1単位当たり5個のint型で表せる大量のデータのリストが欲しくなったとします。
その場合、1単位を表すには以下のケースが考えられます。

case1:5つのデータを持つクラス
1
2
3
4
5
6
7
class TestObject1 {
  public int data1;
  public int data2;
  public int data3;
  public int data4;
  public int data5;
}
case2:int[5]を持つクラス
1
2
3
4
class TestObject2
{
  public int[] datas = new int[5];
}
case3:intの配列
1
int[] datas = new int[ 5 ];
case4:5つのデータを持つ構造体
1
2
3
4
5
6
7
8
struct TestStruct1
{
  public int data1;
  public int data2;
  public int data3;
  public int data4;
  public int data5;
}
上記のケースごとにSystem.Collections.Generic.Listを作成する時間を計測してみました。
追加要素数5,000,000
追加要素10回平均値
(単位:ミリ秒)
Case1: TestObject1528
Case2: TestObject21,084
Case3: int[5]694
Case4: TestStruct176
追加要素数10,000,000
Case11,064
Case22,173
Case31,379
Case4152
追加要素数20,000,000
Case12,005
Case24,131
Case32,682
Case4306
こんな単純なクラスなのに、これほど劇的な違いが生まれてしまいます。

ケース3のList<int[]>ですが、「ふーん、int[]のAddのほうがただのクラスより遲いんだねー、使うのやめとこ」と判断する前に、そもそもこの使い方は誤っています。
実際には5バイトのint[]をリストに追加しまくるのは無駄です。こうするべきです。
Case3A: int[] array = new int[ 5*追加要素数 ];
すると、インスタンス生成に係る回数は1回、必要な時間はほぼゼロ。
上限数が分かっていれば、速度だけで言えばこれ以外の選択肢はありません。
但し、デメリットもまた多いです。
まず、配列だけ見てもなにを表しているのかパッと見わかりません。データの取り扱い方法は工夫する必要が出てくるでしょう。
また、処理中に追加要素数が上限を超えたなら、当然arrayを自力で拡張する必要があります。
素直にCで組んだほうがいいと思います。

また、ケース2は最悪です。自インスタンス生成時間にメンバのint[5]のインスタンス生成時間が加わるため、見事に実行時間がケース1の倍になっています。

ケース4の構造体の早さは圧倒的です。おまけにデータ加工用のメンバ関数などのオブジェクト指向の恩恵も得られるので、わざわざC#を使うならこれを採用したいところです。

自分の場合は、今回はケース1からケース3Aに変更しました。その時の劇的な処理速度向上への驚きがこの記事を書いたきっかけです。おハズカシイ。

2.Math.Ceilingを使ってもそこまで遲くない
私は古い世代なので、浮動小数演算を避けたくなってしまう癖があるのですが、念のため計測してみました。

演算回数5,000,000
関数名10回平均値
(単位:ミリ秒)
整数版CEIL30
Math.Ceiling35
演算回数10,000,000
整数版CEIL62
Math.Ceiling71
演算回数20,000,000
整数版CEIL121
Math.Ceiling143
確かに、ほんのわずかに速いものの、ほとんど変わりません。
但し、上記結果はReleaseビルドでの結果です。Debugビルドでの結果はインライン化がされない等の影響からか整数版CEILは致命的に遲くなります。

演算回数(Debugビルド)5,000,000
関数名10回平均値
(単位:ミリ秒)
整数版CEIL259
Math.Ceiling54
演算回数(Debugビルド)10,000,000
整数版CEIL486
Math.Ceiling108
演算回数(Debugビルド)20,000,000
整数版CEIL973
Math.Ceiling219
素直にMathライブラリを使ったほうがいいですね。

見る人が見たら何言ってんだ、と言われるようなことばかりなのでしょうが、恥をさらす当ブログの趣旨によりここに記録する次第です。
普段使うことがないので、勉強になりました。

ふろく:上記図表の計測プログラム
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
class Program
  {
    static void Main( string[] args )
    {
      test test = new test();
      test.run( 5000000 );
      test.run( 10000000 );
      test.run( 20000000 );
      Console.WriteLine( "Hit ANY KEY" );
      Console.ReadKey();
    }
  }
 
  class test
  {
    class TestObject1 {
      public int data1;
      public int data2;
      public int data3;
      public int data4;
      public int data5;
    }
    class TestObject2
    {
      public int[] datas = new int[5];
    }
    struct TestStruct1
    {
      public int data1;
      public int data2;
      public int data3;
      public int data4;
      public int data5;
    }
 
    List<TestObject1> list1 = new List<TestObject1>();
    List<TestObject2> list2 = new List<TestObject2>();
    List<int[]> list3 = new List<int[]>();
    List<TestStruct1> list4 = new List<TestStruct1>();
    Stopwatch sw = new Stopwatch();
    void Elapse()
    {
      Console.WriteLine( $" {sw.ElapsedMilliseconds}ミリ秒" );
    }
      void case1( int cnt )
    {
      for(int i=0;i< cnt; i++ ) {
        TestObject1 o = new TestObject1();
        o.data1 = i;
        o.data2 = i;
        o.data3 = i;
        o.data4 = i;
        o.data5 = i;
        list1.Add( o );
      }
    }
    void case2( int cnt )
    {
      for ( int i = 0; i < cnt; i++ ) {
        TestObject2 o = new TestObject2();
        o.datas[ 0 ] = i;
        o.datas[ 1 ] = i;
        o.datas[ 2 ] = i;
        o.datas[ 3 ] = i;
        o.datas[ 4 ] = i;
        list2.Add( o );
      }
    }
    void case3( int cnt )
    {
      for ( int i = 0; i < cnt; i++ ) {
        int[] datas = new int[ 5 ];
        datas[ 0 ] = i;
        datas[ 1 ] = i;
        datas[ 2 ] = i;
        datas[ 3 ] = i;
        datas[ 4 ] = i;
        list3.Add( datas );
      }
    }
    void case4( int cnt )
    {
      for ( int i = 0; i < cnt; i++ ) {
        TestStruct1 o;
        o.data1 = i;
        o.data2 = i;
        o.data3 = i;
        o.data4 = i;
        o.data5 = i;
        list4.Add( o );
      }
    }
    void case5( int cnt )
    {
      for ( int i = 0; i < cnt; i++ ) {
        TestStruct1 o;
        o.data1 = i;
        o.data2 = i;
        o.data3 = i;
        o.data4 = i;
        o.data5 = i;
        list4.Add( o );
      }
    }
    int CEIL( int val, int mul10 )
    {
      int ret = val * mul10;
      if ( ret % 10 != 0 ) {
        ret += 10;
      }
      return ret / 10;
    }
    void mathCase1(int cnt)
    {
      for ( int i = 0; i < cnt; i++ ) {
        TestStruct1 o;
        o.data1 = i;
        o.data2 = CEIL( i, 15 );
        o.data3 = CEIL( i, 16 );
        o.data4 = i * 2;
        o.data5 = CEIL( i , 22 );
      }
    }
    void mathCase2( int cnt )
    {
      for ( int i = 0; i < cnt; i++ ) {
        TestStruct1 o;
        o.data1 = i;
        o.data2 = (int)Math.Ceiling( i * 1.5 );
        o.data3 = (int)Math.Ceiling( i * 1.6 );
        o.data4 = i * 2;
        o.data5 = (int)Math.Ceiling( i * 2.2 );
      }
    }
    void interval()
    {
      int WAITSEC = 5;
 
      GC.Collect();
      Console.WriteLine( $"Wait for {WAITSEC}seconds..." );
      Thread.Sleep( WAITSEC * 1000 );
    }
    delegate void cases( int cnt);
    void job(string titile, int cnt, cases c, Object listobj)
    {
      int LOOPCNT = 10;
 
      Stopwatch stopwatch = new Stopwatch();
      MethodInfo mi = null;
      if ( listobj != null ) {
        Type t = listobj.GetType();
        mi = t.GetMethod( "Clear" );
        t.GetProperty( "Capacity" ).SetValue( listobj, cnt, null );
      }
      stopwatch.Start();
      for ( int i = 0; i < LOOPCNT; i++ ) {
        c( cnt );
        if ( listobj != null ) {
          mi.Invoke( listobj, null );
        }
      }
      stopwatch.Stop();
      Console.WriteLine( $"{titile}:{LOOPCNT}回平均値: {stopwatch.ElapsedMilliseconds/ LOOPCNT}ms" );
 
    }
    public void run(int loopcnt)
    {
      Console.WriteLine( $"追加要素数:{loopcnt}" );
      job( "Case1", loopcnt, case1, list1 );
      job( "Case2", loopcnt, case2, list2 );
      job( "Case3", loopcnt, case3, list3 );
      job( "Case4", loopcnt, case4, list4 );
 
      job( "整数版CEIL", loopcnt, mathCase1, null );
      job( "Math.Ceiling", loopcnt, mathCase2, null );
      interval();
    }
  }

0 件のコメント:

コメントを投稿