2010/09/29

C#の構造体の引き渡し方によるパフォーマンスの違い

2016/02/20追記--
yosolaさんの指摘の通り調査用のコードにミスがありました。
このエントリの結果は間違いです。調査用のコードを修正したエントリをご覧ください。
--

メソッドの引数として構造体を渡す場合にちょっと気になったので調査用のコードを書いた。
比較したのは次の3つのケース。
  • 構造体を値渡し
  • 構造体を参照渡し(ref修飾子)
  • 構造体が実装するインターフェースで渡す
コードは次の通り。struct Pointがinterface ITupleを実装している。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace StructInterface
{
    interface ITuple
    {
        int X { get; set; }
        int Y { get; set; }
        int Z { get; set; }
    }

    struct Point : ITuple
    {
        public int X { get; set; }
        public int Y { get; set; }
        public int Z { get; set; }
        public Point(int x, int y, int z) : this() { X = x; Y = y; Z = z; }
        public void AddStruct(Point point)
        {
            X += point.X;
            Y += point.Y;
            Z += point.Z;
        }
        public void AddRefStruct(ref Point point)
        {
            X += point.X;
            Y += point.Y;
            Z += point.Z;
        }
        public void AddInterface(ITuple tuple)
        {
            X += tuple.X;
            Y += tuple.Y;
            Z += tuple.Z;
        }
        public override bool Equals(object obj)
        {
            if (!(obj is Point))
                return false;
            var p = (Point)obj;
            var result = X == p.X && Y == p.Y && Z == p.Z;
            return result;
        }
        public override int GetHashCode()
        {
            var result = X ^ Y ^ Z;
            return result;
        }
        public override string ToString()
        {
            var str = string.Format("{0}, {1}, {2}, ", X, Y, Z);
            return str;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            for (var size = 1000; size <= 1000000; size *= 10)
                Run(size);
        }

        static void Run(int size)
        {
            Console.WriteLine("size: {0}", size);

            var count = 10;
            var sw = new Stopwatch();
            var structTicks = 0L;
            var refStructTicks = 0L;
            var interfaceTicks = 0L;

            for (var c = 0; c <= count; c++)
            {
                var random = new Random(count);
                var points = Enumerable.Range(0, size)
                                       .Select(_ => new Point(random.Next(), random.Next(), random.Next()))
                                       .ToList();

                // Point.AddStruct
                sw.Reset();
                sw.Start();
                var sp = new Point();
                foreach (var point in points)
                    sp.AddStruct(point);
                sw.Stop();
                structTicks += sw.ElapsedTicks;

                // Point.AddRefStruct
                sw.Reset();
                sw.Start();
                var rsp = new Point();
                foreach (var point in points)
                {
                    var p = new Point(point.X, point.Y, point.Z);
                    rsp.AddRefStruct(ref p);
                }
                sw.Stop();
                refStructTicks = sw.ElapsedTicks;

                // Point.AddInterface
                sw.Reset();
                sw.Start();
                var ip = new Point();
                foreach (var point in points)
                    ip.AddInterface(point);
                sw.Stop();
                interfaceTicks += sw.ElapsedTicks;


                // correctness
                if (!sp.Equals(rsp))
                    Console.WriteLine("!sp.Equals(rsp)");
                if (!sp.Equals(ip))
                    Console.WriteLine("!sp.Equals(ip)");
            }

            Console.WriteLine("AddStruct: {0} ({1:F})", structTicks / count, structTicks / (double)structTicks);
            Console.WriteLine("AddRefStruct: {0} ({1:F})", refStructTicks / count, refStructTicks / (double)structTicks);
            Console.WriteLine("AddInterface: {0} ({1:F})", interfaceTicks / count, interfaceTicks / (double)structTicks);
        }
    }
}
ksksts / junk / source — bitbucket.org

結果は次の通り。
size: 1000
AddStruct: 832 (1.00)
AddRefStruct: 31 (0.04)
AddInterface: 789 (0.95)
size: 10000
AddStruct: 1023 (1.00)
AddRefStruct: 119 (0.12)
AddInterface: 1803 (1.76)
size: 100000
AddStruct: 9916 (1.00)
AddRefStruct: 1100 (0.11)
AddInterface: 16277 (1.64)
size: 1000000
AddStruct: 102814 (1.00)
AddRefStruct: 11699 (0.11)
AddInterface: 159081 (1.55)
感想としては、はじめ参照渡しが早いことを意外に感じた(値型と参照型のインスタンス作成の速度差から値型のインスタンスのコピーはそれほどの負荷にならないと思っていた)けど、コピーを発生させるよりは参照渡しにした方が軽いということは納得できた。
インターフェース経由が遅いのは疑問。型変換的な処理が入ってしまうのかな。

メソッドの引数に構造体を渡す場合に(変更しないのに)ref修飾子を使いまくるか、素直にコピーを発生させるか迷う。

2010/03/21

Google Chromeの検索エンジンの設定

Firefoxから移行してある程度経ったのにGoogle Chromeのデフォルトの検索エンジンの設定が気に入らない。悪いというわけじゃなくてFirefoxを使っていた時と同じような検索結果が欲しいから、Google Chromeの検索エンジンの設定を変更してみた。
まずはデフォルトのGoogleでの検索エンジン設定を削除する。
追加するのはGoogleの日本語版と英語版の2つ。

日本語版(google.co.jp):
  • 名前とキーワード: google.co.jp
  • URL: http://www.google.co.jp/search?q=%s&hl=jp&lr=lang_ja&safe=off&ie=utf-8&oe=utf-8&aq=t
非日本語限定版(google.com):
  • 名前とキーワード: google.com
  • URL: http://www.google.com/search?q=%s&hl=en&safe=off
エロ判定なコンテンツが含まれているせいでのフィルタリングを避けたいからsafe=offなんだけど状況によっては画像検索に勇気が必要だったりするからこれは別にするのがいいのかもしれない。


2010/01/23

MSTestExpressionAssertion - VisualStudioの単体テスト用のGallio(MbUnit)のAssertEx.Thatの改造版

VisualStudio 2008の単体テスト機能のカスタマイズ(VS2008 custom assertion example)の続き。
これも放置していたけど一段落つけるまでやった。

前のエントリで書いた通りGallioAssertEx.Thatで構成要素の式ををキャプチャしてその結果を出力できるのがおもしろくて同じようなことをVisualStudioの単体テストで使いたいと思ったのが動機。
いじり方としては、Gallioのソースから欲しいファイルを部分を抜き出してそのまま使ったり変更したりしてMSTestExpressionAssertion.dllという名前のアセンブリを作った。
これに含まれるAssertExクラスのIsTrueメソッドとIsFalseメソッドが、Microsoft.VisualStudio.TestTools.UnitTesting.AssertクラスのIsTrueとIsFalseの式ツリーを受け取るバージョン相当。
MSTestExpressionAssertion.dllを参照に追加して、
AssertEx.IsTrue(() => true)
と書いたテストは成功して、
AssertEx.IsTrue(() => false)
と書いたテストは失敗する。


テストコード:
var p = new Point(1.0, 2.0);
var tole = 1.0e-12;
AssertEx.IsFalse(() => Math.Abs(p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5.0)) <= tole);

エラーメッセージ:
AssertEx.IsFalse failed.
Math.Abs((p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5))) <= tole: True
 Math.Abs((p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5))): 0
  p.CalculateDistance(Point.ORIGIN) - Math.Sqrt(5): 0
   p.CalculateDistance(Point.ORIGIN): 2.23606797749979
    p: (1, 2)
    Point.ORIGIN: (0, 0)
   Math.Sqrt(5): 2.23606797749979
 tole: 1E-12
まあこんなものかと。 式の中でのリテラルが浮動小数点数ではなくなっていたり、結果の出力はToString()メソッドにしているせいでそのあたりにも少し不満があるけど。


テストコード:
var m = Matrix.MakeRotation(Math.PI * 0.25);
var tole = 1.0e-12;
AssertEx.IsFalse(() => m.Transform(new Point(1.0, 0.0)).CalculateDistance(new Point(Math.Sqrt(2.0) * 0.5, Math.Sqrt(2.0) * 0.5)) <= tole);

エラーメッセージ:
AssertEx.IsFalse failed.
m.Transform(new Point(1, 0)).CalculateDistance(new Point((Math.Sqrt(2) * 0.5), (Math.Sqrt(2) * 0.5))) <= tole: True
 m.Transform(new Point(1, 0)).CalculateDistance(new Point((Math.Sqrt(2) * 0.5), (Math.Sqrt(2) * 0.5))): 1.11022302462516E-16
  m.Transform(new Point(1, 0)): (0.70710678118654757, 0.70710678118654746)
   m: (0.70710678118654757, 0.70710678118654757, 
       0.70710678118654746, 0)
   new Point(1, 0): (1, 0)
  new Point((Math.Sqrt(2) * 0.5), (Math.Sqrt(2) * 0.5)): (0.70710678118654757, 0.70710678118654757)
   Math.Sqrt(2) * 0.5: 0.707106781186548
    Math.Sqrt(2): 1.4142135623731
   Math.Sqrt(2) * 0.5: 0.707106781186548
    Math.Sqrt(2): 1.4142135623731
 tole: 1E-12
この程度で既にうるさく感じる。 "Math.Sqrt(2) * 0.5"(とその下の"Math.Sqrt(2)")が2回出力されるのもどうかと思うけど、オブジェクトが変更されるケースも当然あるから同一の式&結果ならまとめるとかいうのもあまりよくない気がする。 細かいけどデフォルトのフォント設定ではmの結果のインデントがずれる(結果が複数行の場合のインデント処理をせっかく入れたのに)。


例外をスローするテストコード:
AssertEx.IsTrue(() => string.Format("{0}", null) == "");

エラーメッセージ:
テスト メソッド MSTestExpressionAssertionTest.AssertExSample.Test02 は例外をスローしました:  System.ArgumentNullException: 値を Null にすることはできません。
パラメータ名: args。
スタックトレース:
System.String.Format(IFormatProvider provider, String format, Object[] args)
lambda_method(ExecutionScope )
Gallio.Common.Linq.ExpressionInstrumentor.Intercept[T](Expression expr, Func`1 continuation)
Intercept[T](Expression expr, Func`1 continuation)
Gallio.Common.Linq.ExpressionInstrumentor.InterceptNonVoid[T](Expression expr, Func`1 continuation)
lambda_method(ExecutionScope )
Gallio.Common.Linq.ExpressionInstrumentor.Intercept[T](Expression expr, Func`1 continuation)
Intercept[T](Expression expr, Func`1 continuation)
Gallio.Common.Linq.ExpressionInstrumentor.InterceptNonVoid[T](Expression expr, Func`1 continuation)
lambda_method(ExecutionScope )
Eval(Expression`1 condition)
MSTestExpressionAssertion.AssertEx.IsTrue(Expression`1 condition, String message, Object[] parameters)
MSTestExpressionAssertion.AssertEx.IsTrue(Expression`1 condition)
MSTestExpressionAssertionTest.AssertExSample.Test02() C:\home\development\projects\bitbucket\junk\cs\MSTestExpressionAssertion\MSTestExpressionAssertionTest\AssertExSample.cs 内: 行 42
スローされた例外クラスはSystem.ArgumentNullExceptionクラス。
例外クラスの型とスタックトレースの両方をうまく維持する方法が分らなかった(というか多分ない)から、スローされる例外クラスを優先した。そのためスタックトレースにMSTestExpressionAssertionのコードも含まれてしまっている。


Gallioからの主な変更内容:
  • 式ツリーに含まれる定数式以外のすべての式とその結果を出力するようにした
  • 式の中で発生した例外はcatchせずにそのまま挙げるようにした
  • 結果の出力はToString()メソッドを使うようにした
その他感想とか:
  • すべての式を出力するための式のフォーマッタが面倒だった。GallioのExpressionFormattingRule.csをベースにしたExpressionFormatter.cs(と補助的にExpressionExtensions.cs)がその部分。staticメンバやthisのメンバの判定処理についてはもっとうまい方法があるかも。
  • デバッガで楽にテストの式にステップインできるようにMSTestExpressionAssertionプロジェクトのReleaseビルドでは/debug:none指定。デバッグシンボルがないアセンブリを読み込むと警告を表示するのがデフォルトなのがうざい。対象のコードにDebuggerStepThroughAttributeDebuggerHiddenAttributeDebuggerNonUserCodeAttributeを指定しようかと思ったけどGallioのコードを変更するのを避けるため止めた。
  • でも式が連続して実行される感じではなくなっている(結果を取得するため分解して書き換えているから)のでステップ実行が微妙。まあ使えなくはない。
  • それにしてもExpressionInstrumentor.csExpressionFormattingRule.csはうまい。こんなのよく書くなと同時によく書けるなと思う。
ソースはBitBucketのMSTestExpressionAssertion
Gallioのソースからの変更部分はmodified-files.diff

2010/01/16

MiniTwitter-ma - MiniTwitterの複数アカウント対応版+α

MiniTwitterの複数アカウント対応の続き。
放置していたけど手をつけたからには一段落するまでやっておく。

(まめ)しばやんさんのMiniTwitterのバージョン1.05.2(たぶんchangeset 62377)をベースにして複数アカウント対応(同時に扱えるのではなくアカウントを切り替えられるという感じ)とたくさんのポストを表示できるタイムライン表示機能を追加してみた。

複数アカウント対応については次の通り。
「What's happening?」の右側に現在のアカウントを表示するボタンを追加。このボタンをクリックするとアカウントを選択するメニューがポップアップされる。
複数のアカウントの登録できるように設定ダイアログを変更。




タイムライン表示の変更については次の通り。
スクリーンショットは「標準」と「タイト」。どちらもテーマは「ネットブック最適化」を選択。
「タイト」の表示は「標準」をベースにしてフォントサイズと行の高さとマージンを小さめに変更したもの。



ソースはBitbucketのMiniTwitter-ma。オリジナルのMiniTwitterからの変更箇所はmodified-files.diff
ライセンスはオリジナルと同じくApache License, Version 2.0
使う人がいるとはあまり思わないけど一応ビルド済みバイナリをWindows LiveのSkyDriveに置いた

やってみた感想:
  • WPFおもしろい(しばやんさん&MiniTwitterに感謝)。
  • Apache Licenseの4-2が微妙。変更したファイルの中に変更箇所を明示する必要はなく、変更したファイルがあればそれを明示しろと解釈した(※それでは条項を満たさないと思われる場合は教えてください)。
  • 相場関連を別アカウントでやっていたから複数アカウント対応が欲しかったけどアカウントを分けない方が良いように思えてきた。分けたときは「人生オワタ\(^o^)/」とか素人丸出しなポストと結び付けたくないなと思っていたけど、前者になるような取引をしそうにないし、後者はそもそも素人レベルなんだからそれを隠そうとするのもどうよって思うようになってきた。フォローする人たちが全然違うけどアカウントを分けるのを止めてまとめようかと思う。
  • オリジナルのMiniTwitterのタイムライン表示はスペースを贅沢に使いすぎ、件数少なすぎ、どんだけの解像度で使っているんだよw、というくらいに思っていたけど自然に作るとそんな感じになると思った。
  • その上で余白少なめ、行間小さめなタイムライン表示を追加したらEee PC 900-Xでも使えなくないな、と。ただしUIデザインセンスのない効率性重視(<-機能的に必要なものを詰め込む傾向)だなぁ。
  • いじってみた上でやっぱりWPF面白い。学習が必要。
@yuki1090さんMiniTwitter勝手に改造版のテキスト選択できるようにFlowDocumentを使うのはいいなぁ…と思って今見てみたらなんか作成中プロジェクトがある。期待してみる。