Visual Basic 8 と "type" の省略,ついでに DLR とパフォーマンス

今回は時間がないので本当にメモ.


基本的な本能 ラムダ式

ラムダ式については C# の実相に関しては Nyaruru さん のブログなんかでよく目にするのですが、Visual Basic の例はほとんど目にしないんですよね。

で、この記事を見ていて思ったのはやっぱり VB + LINQ の環境(もちろんLambda含む)では Option Strict は OFF が推奨されていくのかなぁという空気。

確かに実行時に型を決めるというのが効果的な部分があるのは認めるんですが、一度決めた型をいくらでも変更できてしまうというのがどうにも気持ち悪い感じはします。

(実行時に型を決めたとして、それに他の型の変数を代入しようとしたら例外が起きるという動きならまだ許せる)

MSDN Magazine 9 月号のその記事は私も読んではいたのですが,読んだ上での最大の収穫が「自分は Visual Basic の文法を知らないということが分かった」なのでネタにするか迷っていたというのが本音.
MSDN Magazine の記事を読みながら一番ひっかかったのはこの辺.

遅延バインディング — オブジェクトを推定する

このシナリオでは、lambda 変数もラムダ式も型指定されません。

Dim lambda = Function(x) x * x

ここでは、匿名デリゲートもコンパイラによって生成されますが、lambda の型は System.Object です。これは、このシナリオで遅延バインディングが有効になるのは、Option Strict が off の場合であることを意味します。

これは、遅延バインディングに依存している人にとっては非常に好都合なシナリオです。遅延バインディング処理は、ラムダ式で完全にサポートされているので、上の例では、* 演算子が lambda に指定する型で定義されている限り有効です。

Dim a = lambda(10)
Dim b = lambda(CDec(10))
Dim c = lambda("This will throw an exception because " & _
               "strings don't support the * operator")

この例でわかるように、ランタイム型に * 演算子がある限り、すべて適切に機能します。このラムダ式の特性は、Visual Basic の遅延バインディング モデルに非常にうまく適合します。

こういうコードが良い悪いを判断する以前に,そもそも Object 型同士のかけ算とか足し算を最近の Visual Basic がどう扱っているかすら知らなかった私,みたいな.
Option Strict はどっちを推奨とか判断できる以前に,コードが読めるかどうかというレベルなのですな.

関数定義における省 "typing"

Visual Basic 8 で,"typing" をなるべく省略すると関数定義がどうなるかの実験.

Option Strict Off

'Module 中での関数定義を想定
Function myadd(ByVal a, ByVal b)
    Return a + b
End Function

この記法自体にはそれほど変なところはないと思います.Python だってこんな感じなわけですし.

def myadd(a,b):
    return a+b

静的型付け言語からも 2 つほど.まず C++.引数と戻り値の型が同じという制約が入っているので,Python 版と全く同じ自由度をもつ訳ではないですが.

template<typename T>
T myadd(T a, T b)
{
    return a + b;
}

さらに Haskell.勝手に "myadd :: (Num a) => a -> a -> a" と推論してくれます.

myadd a b = a + b

Visual Basic の 省 "typing" 足し算を詳しく見てみる

Visual Basic 8 のコンパイラが何を行っているのか,コンパイル結果を Reflector で C# に変換してみます.

public static object myadd(object a, object b)
{
    return Microsoft.VisualBasic.CompilerServices.Operators.AddObject(a, b);
}

どうやら Microsoft.VisualBasic.CompilerServices 名前空間の Operators クラスに定義された AddObject メソッドに丸投げのようです.
AddObject メソッドで実験するために IronPython (iyp.exe) 起動.

import clr
from System import *
clr.AddReference("Microsoft.VisualBasic")

from Microsoft.VisualBasic.CompilerServices import *

あとは気が済むまで色々放り込んでみます.

>>> Operators.AddObject(1,2)
3
>>> Operators.AddObject(1,"2")
3.0
>>> Operators.AddObject(TimeSpan(), TimeSpan.FromDays(1))
<System.TimeSpan object at 0x000000000000002B [1.00:00:00]>
>>> Operators.AddObject(TimeSpan(), TimeSpan.FromDays(1.4))
<System.TimeSpan object at 0x000000000000002C [1.09:36:00]>
>>> Operators.AddObject(1,"a")
Traceback (most recent call last):
  File Microsoft.VisualBasic, line unknown, in ToDouble
  File Microsoft.VisualBasic, line unknown, in ParseDouble
  File , line 0, in <string>##22
  File , line 0, in _stub_##15
  File Microsoft.VisualBasic, line unknown, in AddObject
  File Microsoft.VisualBasic, line unknown, in ToDouble
TypeError: String "a" から型 'Double' への変換は無効です。

だいたい状況が分かったところで,ドキュメント.

ついでに気まぐれで作ってみた Mathematica によるエミュレーション.

Unprotect[Plus];
Plus[a_String, b_String] := a <> b;
Plus[a_ /; NumberQ[a], b_String /; StringMatchQ[b, NumberString]] := 
  a + (ToExpression[b] + 0.0);
Protect[Plus];

パフォーマンス

ここで軽くマイクロベンチマーク

C# 2.0
using System;
using System.Diagnostics;

static class Program
{
    static TimeSpan myadd(TimeSpan a, TimeSpan b)
    {
        return a + b;
    }

    static TimeSpan skipdays(int n)
    {
        TimeSpan a = new TimeSpan();
        TimeSpan b = TimeSpan.FromDays(1.0);
        for (int i = 0; i < n; ++i)
            a = myadd(a, b);
        return a;
    }

    static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        TimeSpan ret = skipdays(100000);
        sw.Stop();
        Console.WriteLine(ret);
        Console.WriteLine(sw.Elapsed);
    }
}
Visual Basic 8
Module Module1

    Function myadd(ByVal a, ByVal b)
        Return a + b
    End Function

    Function skipdays(ByVal n)
        Dim a = New TimeSpan
        Dim b = TimeSpan.FromDays(1.0)
        Dim i As Integer
        For i = 0 To n-1
            a = myadd(a, b)
        Next
        Return a
    End Function

    Sub bench()
        Dim sw As Stopwatch = New Stopwatch()
        sw.Start()
        Dim ret = skipdays(100000)
        sw.Stop()
        Console.WriteLine(ret)
        Console.WriteLine(sw.Elapsed)
    End Sub

    Sub Main()
        bench()
    End Sub

End Module
IronPython
from System import *

def myadd(a,b):
    return a+b

def skipdays(n):
    a=TimeSpan()
    b=TimeSpan.FromDays(1.0)
    for i in range(0,n):
        a=myadd(a,b)
    return a

def bench():
    from System.Diagnostics import *
    sw = Stopwatch()
    sw.Start()
    ret = skipdays(100000)
    sw.Stop()
    print ret
    print sw.Elapsed

bench()

環境.

結果.
(追記)コメントでご指摘があったように C# のコードで Stopwatch を止めていなかったのを修正しました.当初 C# 版は 5 msec となっていましたが,再度計測し直して 4 msec に修正しました.

Language Time
Visual Basic 8 2269 msec
IronPython 2.0 beta3 56 msec
C# 2.0 4 msec

IronPyton の結果は,ipy.exe を実行して対話型環境にコードを貼り付けたものです.
Visual Basic 8 と IronPython は,ソースコードの上では似ていても,パフォーマンスは大きく異なっているのが目を引きます.

まとめ

  • 「Option Strict Off は遅いのも嫌だ」という主張の背景を垣間見た.
  • 似たようなコードが書けることと,似たようなパフォーマンスが得られることは,似て非なるもの.
  • DLR はパフォーマンスに関して結構いいとこ取りができている(かも?)

追記

C# 3.0と同じように Visual Basic 9 でも型推論が働くので,次のようなコードはラムダ式の引数と戻り値の型を省略することができます.この場合は自明だから省略するであって,省略して Object 型になるというわけではないと.

Sub Main()
    Dim result = Enumerable.Range(0, 10) _
                 .Where(Function(x) x Mod 2 = 0) _
                 .Select(Function(x) x * 20)

    For Each x In result
        Console.WriteLine(x)
    Next
End Sub

最初に紹介した MSDN Magazine の記事では「基本的な本能 ラムダ式: 型の推定」の章でこの部分が解説されています.