tech.guitarrapc.cóm

Technical updates

Sharplab のMemoryGraph を使ってメモリの状態を確認する

以前TryRoslynと言われてたサービスですが、今はSharplabという名になっています。

このサービスを使うと、コードがILやネイティブコードにどのようにコンパイルされるか確認したり、実行したりオブジェクトのメモリ状態を確認できます。

例えば次の図は、構造体の文字列がどのようなメモリ状態なのかを示したものです。

Sharplabを使って、コードだけでなくメモリ状態を可視化することで理解を深めるきっかけにできるか見てみましょう。

概要

Shaplabを使って可視化することで「分からないことを、何が分からないかわかるようにできる」ので活用するといいです。

LinqPadも似てますが、併用するとより幸せになれます。

Sharplabの基本

Sharplabにアクセスしてみましょう。

https://sharplab.io/

次のようなシンプルなクラスが表示されています。

using System;
public class C {
    public void M() {
    }
}

ここに色々コードを書いて試すことができます。

いつ使うのか

スマホやPCでも入力に困らないレベルでインテリセンスによるコード入力補助があります。さらに後述するコードの実行、解析、C#バージョンの選択までできます。

加えて、入力したコードが自動的に一意なurlで再表示できるので、コードの共有にもいいでしょう。

だいたいここまでできるとLinqPadでできることが網羅されており、nugetを使わない限りはLinqPadより気軽で便利な面も多いでしょう。

基本的な文法、Decompileや言語バージョンの違いの確認程度なら、Sharplabでいい感じがします。

インテリセンス

コードの共有

何かしらコードを入力するとURLがついて、入力ごとに変化しているのが分かります。

一意なURLがコードごとに生成される

このURLを踏むとそのコードを表示できるのでコードで状態を共有出来て非常に便利です。実際、C# のGitHub IssueなどでSharpLabのURLで再現コードをシェアしているのも見かけます。

この記事でもコードごとにURLをシェアしてみましょう。

言語選択

画面上部のCodeからC# 以外にVisual Basic.NETやF# が選べます。

言語選択

表示の切り替え(Decompile)

下(あるいは右)ペインResultsを選ぶといくつか選択肢があります。Decompileのまとまりから見ていきましょう。

Decompileの中でC# を選ぶとデコンパイルしたC#コード、ILを選ぶとIL状態、JIT Asmを選ぶとネイティブコードを確認できます。

Decompile C#

Decompile IL

Decompile JIT Asm

言語ごとのデコンパイル結果の比較

余談ですが、C#、Visual Basic.NET、F# それぞれで同じようなコードを書いた時のデコンパイル結果を最も簡単に確認できるサービスの1つです。

C# の次のコードを使ってデコンパイル結果を見てみましょう。

シンプルなクラスのコード

using System;
public class C {
    public void M() {
    }
}
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class C
{
    public void M()
    {
    }
}

Visual Basic.NETを選んで表示されるC# Decompile結果はC# とほぼ同じですね。

Imports System
Public Class C
    Public Sub M()
    End Sub
End Class
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyVersion("0.0.0.0")]
public class C
{
    public void M()
    {
    }
}

F# だとガラッと雰囲気違うのは興味深いです。

open System
type C() =
    member this.M() = ()
using Microsoft.FSharp.Core;
using System;
using System.Reflection;

[assembly: FSharpInterfaceDataVersion(2, 0, 0)]
[assembly: AssemblyVersion("0.0.0.0")]
[CompilationMapping(SourceConstructFlags.Module)]
public static class _
{
    [Serializable]
    [CompilationMapping(SourceConstructFlags.ObjectType)]
    public class C
    {
        public C()
        {
            ((object)this)..ctor();
        }

        public void M()
        {
        }
    }
}
namespace <StartupCode$_>
{
    internal static class $_
    {
    }
}

F#からC# Decompile

Other

表示切替の中に、Decompile以外にOtherがあります。

Syntax もその1つで、Sharplabを使うことでRoslynによるSyntaxTreeの状態も確認できます。

SyntaxTree状態

Roslyn拡張を書くときなどににらめっこしたりすることがありますが、LinqPad以外でもきれいにみれるのはうれしいものがあります。

Verify Only を選ぶことでコンパイルできるかの確認ができます。そもそもコード入力中にがしがし評価されてエラーがずらずらでるので、Verifyしなくとも気付けるのですが。

Verify

Explain を選ぶと、余り使われないシンタックスの説明が表示されます。例えば、予約語を変数名に使う@varシンタックスを使ってみましょう。

@varのコード

using System;
public class C {
    public void M() {
        var @var = "";
    }
}
@var is a verbatim identifier. [Docs]
Prefix @ allows keywords to be used as a name (for a variable, parameter, field, class, etc).
For example, M(string string) doesn't compile, while M(string @string) does.

Explainによる利用頻度の低いシンタックスの説明

インタフェースのデフォルト実装など、新しい機能はまだ説明がないのでちまちまIssueを見るといいです。

interface Hoge {
    string Text {get;set;}

    public void Fuga()
    {
        Console.WriteLine(Text);
    }
}

https://github.com/ashmind/language-syntax-explanations/issues

Run

コードを実行できるので、LinqPadで軽く実行するのをWebで試せるので、C# の書き捨てコードとしては相当楽なのではないでしょうか。 この時、結果表示にはConsole.WriteLine()によるstdout以外にも、<何かしらのオブジェクト>.Inspect()拡張メソッドでも表示できます。Inspectメソッドは、LinqPadの.Dump()に近い感覚でグラフィカルにオブジェクトが表示されるのでいい感じです。

上 Console.WriteLine()、下 .Inspect() 拡張メソッド

他にもヒープ状態の確認に使えるInspect.Heap()メソッドや、スタック状態の確認に使えるInspect.Stack()メソッドがあります。

Runによるコード実行

using System;
// Run mode:
//   value.Inspect()      — often better than Console.WriteLine
//   Inspect.Heap(object) — structure of an object in memory (heap)
//   Inspect.Stack(value) — structure of a stack value
public static class Program {
    public static void Main() {
        Console.WriteLine("🌄");
        new [] {"hoge", "moge"}.Inspect();
        Inspect.Heap(new [] {"pi", "yo"});
        Inspect.Stack(new [] {1, 2});
    }
}

Runの表示結果

C# のメモリ状態を確認する

さて、この記事の本筋に戻りましょう。C# を書いているとコードからは挙動やILを見ないと判断に迷うケースは少なからずあります。私は少なくともたくさんあります。

Visual StudioやVS Codeでパッと思いつくいい感じのやり方がなく、私はもっぱらLinqPadでコードを実行しつつILを見たりして確認していました。UnityのProfilerを使うという手もありますが、少しノイズが多くやりにくさはぬぐえません。

メモリの状態を確認するには、メモリの状態が可視化されると直感的ですが、それがない/そこまでが遠いのですね。

そんな時にSharplabの`Inspect.MemoryGraph()メソッドを使うとメモリマップが可視化され、わかりやすさを大いに助けてくれます。早速これを使ってBoxing (ボックス化) と、構造体におけるstringの参照のされ方、クラスではどう変わるのかを見てみましょう。

Boxing を可視化する

Boxingは、C# で起こりやすい割に案外分かりにくいと感じる状態の1つです。説明は適当に資料に譲るとして、intをボックス化してobjectに代入した状態を考えてみましょう。

https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/types/boxing-and-unboxing

int をボックス化してobjectに代入したコード

using System;
public static class Program {
    public static void Main() {
        var x = 5;
        object y = x;
        Inspect.MemoryGraph(y);
    }
}

明らかに普段書かないコードのように見えますが、場合によっては書きます。ここまでわかりやすいとパット頭でBoxing状態が書けそうですが、実際に思い描いたものと一緒か見るのに変数yInspect.MemoryGraph(o)として可視化してみましょう。

intをボックス化してobjectに代入した状態

では、int配列をobject配列でボックス化するとどうなるでしょうか。

int をボックス化してobject配列に代入した

using System;
public static class Program {
    public static void Main() {
        var o = new object[] {1, 2, 3};
        Inspect.MemoryGraph(o);
    }
}

変数oInspect.MemoryGraph(o)として可視化してみましょう。

Boxingの可視化

戻り値がvoidだとInspectしようがないので可視化できず、次のコードはエラーになります。

voidはInspectできない

using System;
public static class Program {
    public static void Main() {
        Inspect.MemoryGraph(Console.WriteLine(1));
    }
}

「intがobjectでボックス化されている」と言葉で聞いて、コードでも書けるときはいいのですが、「これもしかしてボックス化してる?」という時にサクッと書いて、メモリの状態を可視化できるのはかなり便利です。

構造体における文字列の参照状態を確認する

こんな疑問があります。

じんぐる (@xin9le) February 12, 2019

ILを見ればすぐにわかるケースですがSharplabを使えば可視化されます。次のコードを模擬コードとしてみてみましょう。値型のintも持たせて参照型のstringとどう違うかついでに確認します。

構造体で文字列を持たせたときのメモリ状態

public static class Program {
    public static void Main() {
        var s = new S();
        s.text = "before";
        s.num = 1;

        var t = s.text;

        var h = s;
        h.num = 2;

        s.text = "after";

        Inspect.MemoryGraph(s, t, h);
    }

    struct S
    {
        public string text;
        public int num;
    }
}

構造体における参照状態の確認

構造体は、参照型であるstring(文字列) を参照で持っていることがrefからわかりますね。 変数sは、afterという文字列を代入しているので別の文字列を参照していることが分かります。

ちなみにLinqPadで結果を見ると次のようになります。

LinqPadによる構造体の状態確認

IL見れば、なるほどldlocaLdfld

IL_0000:  nop
IL_0001:  ldloca.s    00 // s
IL_0003:  initobj     UserQuery.S
IL_0009:  ldloca.s    00 // s
IL_000B:  ldstr       "before"
IL_0010:  stfld       UserQuery+S.text
IL_0015:  ldloca.s    00 // s
IL_0017:  ldc.i4.1
IL_0018:  stfld       UserQuery+S.num
IL_001D:  ldloc.0     // s
IL_001E:  ldfld       UserQuery+S.text
IL_0023:  stloc.1     // t
IL_0024:  ldloc.0     // s
IL_0025:  stloc.2     // h
IL_0026:  ldloca.s    02 // h
IL_0028:  ldc.i4.2
IL_0029:  stfld       UserQuery+S.num
IL_002E:  ldloca.s    00 // s
IL_0030:  ldstr       "after"
IL_0035:  stfld       UserQuery+S.text
IL_003A:  ldloc.0     // s
IL_003B:  call        LINQPad.Extensions.Dump<S>
IL_0040:  pop
IL_0041:  ldloc.1     // t
IL_0042:  call        LINQPad.Extensions.Dump<String>
IL_0047:  pop
IL_0048:  ldloc.2     // h
IL_0049:  call        LINQPad.Extensions.Dump<S>
IL_004E:  pop
IL_004F:  ret

クラスにおける参照状態を確認する

先ほどの構造体Sを、クラスにするとどうなるか見てみましょう。

クラスで文字列を持たせたときのメモリ状態

public static class Program {
    public static void Main() {
        var s = new S();
        s.text = "before";
        s.num = 1;

        var t = s.text;

        var h = s;
        h.num = 2;

        s.text = "after";

        Inspect.MemoryGraph(s, t, h);
    }

    class S
    {
        public string text;
        public int num;
    }
}

クラスにおける参照状態の確認

構造体と違い、それぞれの値が参照でつながっているのが分かりますが、分かりにくいですね! LinqPadで実行すると、クラスの時と構造体の時で結果が変わっていることが分かります。

LinqPadによるクラスの状態確認

違いは次の通りです。

オブジェクト 構造体 クラス
s.text after after
s.num 1 2
t before before
h.text before after
h.num 2 2

コードとメモリの状態を見ると理由は明らかです。

構造体における参照状態の確認
クラスにおける参照状態の確認

ShapLabで楽しいC# コーディングを過ごしましょう!