tech.guitarrapc.cóm

Technical updates

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

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

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

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

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

目次

TL;DR;

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

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

Sharplabの基本

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

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# 以外に VB.NET や F# が選べます。

言語選択

表示の切り替え(Decompile)

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

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

Decompile C#

Decompile IL

Decompile JIT Asm

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

余談ですが、C#、VB.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()
    {
    }
}

VB.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);
    }
}

github.com

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に代入した状態を考えてみましょう。

docs.microsoft.com

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でボックス化されている」と言葉で聞いて、コードでも書けるときはいいのですが、「これもしかしてボックス化してる?」という時にサクッと書いて、メモリの状態を可視化できるのはかなり便利だと思います。

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

こんな疑問があります。

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# コーディングを過ごしましょう!