การมีตัวแปรแบบไดนามิกส่งผลต่อประสิทธิภาพอย่างไร


128

ฉันมีคำถามเกี่ยวกับประสิทธิภาพของdynamicC # ฉันอ่านแล้วdynamicทำให้คอมไพเลอร์ทำงานอีกครั้ง แต่มันทำอย่างไร

ต้องคอมไพล์เมธอดทั้งหมดด้วยdynamicตัวแปรที่ใช้เป็นพารามิเตอร์หรือเฉพาะบรรทัดที่มีพฤติกรรม / บริบทแบบไดนามิก?

ฉันสังเกตเห็นว่าการใช้dynamicตัวแปรสามารถทำให้การวนซ้ำแบบง่ายๆช้าลงได้ 2 คำสั่งขนาด

รหัสที่ฉันเล่นด้วย:

internal class Sum2
{
    public int intSum;
}

internal class Sum
{
    public dynamic DynSum;
    public int intSum;
}

class Program
{
    private const int ITERATIONS = 1000000;

    static void Main(string[] args)
    {
        var stopwatch = new Stopwatch();
        dynamic param = new Object();
        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        Console.ReadKey();
    }

    private static void Sum(Stopwatch stopwatch)
    {
        var sum = 0;
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch, dynamic param)
    {
        var sum = new Sum2();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
    }

    private static void DynamicSum(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.DynSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

ไม่มันไม่เรียกใช้คอมไพเลอร์ซึ่งจะทำให้การลงโทษช้าในการส่งครั้งแรก ค่อนข้างคล้ายกับ Reflection แต่มีสมาร์ทมากมายในการติดตามสิ่งที่ทำมาก่อนเพื่อลดค่าใช้จ่าย "รันไทม์ภาษาแบบไดนามิก" ของ Google สำหรับข้อมูลเชิงลึกเพิ่มเติม และไม่มันจะไม่เข้าใกล้ความเร็วของลูป 'เนทีฟ'
Hans Passant

คำตอบ:


235

ฉันอ่านว่าไดนามิกทำให้คอมไพเลอร์ทำงานอีกครั้ง แต่มันทำอย่างไร ต้องคอมไพล์เมธอดทั้งหมดใหม่ด้วยไดนามิกที่ใช้เป็นพารามิเตอร์หรือมากกว่าบรรทัดเหล่านั้นที่มีพฤติกรรม / บริบทแบบไดนามิก (?)

นี่คือข้อตกลง

สำหรับทุกนิพจน์ในโปรแกรมของคุณที่เป็นประเภทไดนามิกคอมไพลเลอร์จะปล่อยรหัสที่สร้าง "วัตถุไซต์การเรียกแบบไดนามิก" เดียวที่แสดงถึงการดำเนินการ ตัวอย่างเช่นหากคุณมี:

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();

จากนั้นคอมไพเลอร์จะสร้างโค้ดที่มีศีลธรรมเช่นนี้ (รหัสจริงค่อนข้างซับซ้อนกว่าเล็กน้อยซึ่งทำให้ง่ายขึ้นเพื่อวัตถุประสงค์ในการนำเสนอ)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);

ดูวิธีการทำงานจนถึงตอนนี้? เราสร้างไซต์การโทรเพียงครั้งเดียวไม่ว่าคุณจะโทรหา M กี่ครั้งก็ตามไซต์การโทรจะอยู่ตลอดไปหลังจากที่คุณสร้างครั้งเดียว ไซต์การโทรเป็นวัตถุที่แสดงถึง "จะมีการเรียก Foo แบบไดนามิกที่นี่"

ตกลงตอนนี้คุณมีไซต์การโทรแล้วการเรียกใช้งานอย่างไร?

ไซต์การโทรเป็นส่วนหนึ่งของ Dynamic Language Runtime DLR ระบุว่า "อืมมีใครบางคนกำลังพยายามเรียกใช้เมธอด foo บนอ็อบเจ็กต์ที่นี่แบบไดนามิกฉันรู้อะไรเกี่ยวกับสิ่งนั้นหรือไม่ไม่งั้นฉันจะรู้ดีกว่า"

จากนั้น DLR จะซักถามวัตถุใน d1 เพื่อดูว่ามีอะไรพิเศษหรือไม่ อาจเป็นวัตถุ COM ดั้งเดิมหรือวัตถุ Iron Python หรือวัตถุ Iron Ruby หรือวัตถุ IE DOM ถ้าไม่ใช่สิ่งเหล่านั้นก็ต้องเป็นวัตถุ C # ธรรมดา

นี่คือจุดที่คอมไพเลอร์เริ่มทำงานอีกครั้ง ไม่จำเป็นต้องมีตัวเล็กเซอร์หรือตัวแยกวิเคราะห์ดังนั้น DLR จึงเริ่มต้นคอมไพเลอร์ C # เวอร์ชันพิเศษที่มีเพียงตัววิเคราะห์ข้อมูลเมตาตัววิเคราะห์ความหมายสำหรับนิพจน์และตัวปล่อยที่ปล่อย Expression Trees แทน IL

ตัววิเคราะห์ข้อมูลเมตาใช้การสะท้อนเพื่อกำหนดประเภทของวัตถุใน d1 จากนั้นส่งผ่านไปยังเครื่องวิเคราะห์ความหมายเพื่อถามว่าจะเกิดอะไรขึ้นเมื่อมีการเรียกใช้วัตถุดังกล่าวบนเมธอด Foo ตัววิเคราะห์ความละเอียดเกินจะคำนวณออกมาแล้วสร้าง Expression Tree - เหมือนกับที่คุณเรียก Foo ในนิพจน์แลมบ์ดาซึ่งแสดงถึงการเรียกนั้น

จากนั้นคอมไพเลอร์ C # จะส่งแผนภูมินิพจน์นั้นกลับไปที่ DLR พร้อมกับนโยบายแคช นโยบายมักจะเป็น "ครั้งที่สองที่คุณเห็นวัตถุประเภทนี้คุณสามารถใช้โครงสร้างนิพจน์นี้ซ้ำได้แทนที่จะเรียกฉันกลับมาอีกครั้ง" จากนั้น DLR จะเรียกคอมไพล์บนแผนภูมินิพจน์ซึ่งเรียกใช้คอมไพเลอร์นิพจน์ - ทรีเป็น IL และแยกบล็อกของ IL ที่สร้างขึ้นแบบไดนามิกในผู้ร่วมประชุม

จากนั้น DLR จะแคชผู้รับมอบสิทธิ์นี้ในแคชที่เกี่ยวข้องกับอ็อบเจ็กต์ไซต์การโทร

จากนั้นจะเรียกผู้รับมอบสิทธิ์และการเรียก Foo จะเกิดขึ้น

ครั้งที่สองที่คุณโทรหา M เรามีไซต์โทรอยู่แล้ว DLR จะซักถามอ็อบเจ็กต์อีกครั้งและถ้าอ็อบเจ็กต์เป็นประเภทเดียวกับครั้งที่แล้วอ็อบเจ็กต์จะดึงผู้รับมอบสิทธิ์ออกจากแคชและเรียกใช้อ็อบเจ็กต์ หากวัตถุเป็นประเภทอื่นแคชจะหายไปและกระบวนการทั้งหมดจะเริ่มต้นใหม่อีกครั้ง เราทำการวิเคราะห์ความหมายของการโทรและเก็บผลลัพธ์ไว้ในแคช

สิ่งนี้เกิดขึ้นกับทุกนิพจน์ที่เกี่ยวข้องกับไดนามิก ตัวอย่างเช่นหากคุณมี:

int x = d1.Foo() + d2;

จากนั้นจะมีไซต์การโทรแบบไดนามิกสามไซต์ หนึ่งสำหรับการเรียกแบบไดนามิกไปยัง Foo หนึ่งสำหรับการเพิ่มไดนามิกและอีกรายการหนึ่งสำหรับการแปลงไดนามิกจากไดนามิกเป็น int แต่ละคนมีการวิเคราะห์รันไทม์ของตัวเองและแคชผลการวิเคราะห์ของตัวเอง

เข้าท่า?


ด้วยความอยากรู้อยากเห็นเวอร์ชันคอมไพเลอร์พิเศษที่ไม่มี parser / lexer ถูกเรียกใช้โดยการส่งแฟล็กพิเศษไปยัง csc.exe มาตรฐาน?
Roman Royter

@ เอริกฉันขอรบกวนคุณช่วยชี้ให้ฉันดูบล็อกโพสต์ก่อนหน้าของคุณที่คุณพูดถึงการแปลงโดยนัยของ short, int ฯลฯ ได้ไหม อย่างที่ฉันจำได้ว่าคุณพูดถึงในนั้นว่าทำไม / ทำไมการใช้ไดนามิกกับ Convert ToXXX ทำให้คอมไพเลอร์เริ่มทำงาน ฉันแน่ใจว่าฉันกำลังหารายละเอียด แต่หวังว่าคุณจะรู้ว่าฉันกำลังพูดถึงอะไร
Adam Rackis

4
@ โรมัน: ไม่ csc.exe เขียนด้วย C ++ และเราต้องการบางสิ่งที่เรียกได้ง่ายๆจาก C # นอกจากนี้คอมไพเลอร์เมนไลน์ยังมีอ็อบเจ็กต์ประเภทของตัวเอง แต่เราจำเป็นต้องสามารถใช้อ็อบเจ็กต์ประเภท Reflection เราแยกส่วนที่เกี่ยวข้องของรหัส C ++ จากคอมไพเลอร์ csc.exe และแปลทีละบรรทัดเป็น C # จากนั้นสร้างไลบรารีขึ้นมาเพื่อให้ DLR เรียกใช้
Eric Lippert

9
@Eric "เราแยกส่วนที่เกี่ยวข้องของโค้ด C ++ จากคอมไพเลอร์ csc.exe และแปลทีละบรรทัดเป็น C #" เป็นเรื่องเกี่ยวกับที่ผู้คนคิดว่า Roslyn ควรค่าแก่การติดตาม :)
ShuggyCoUk

5
@ShuggyCoUk: ความคิดในการมีคอมไพเลอร์-as-a-service เริ่มเกิดขึ้นมาระยะหนึ่งแล้ว แต่จริงๆแล้วการต้องการบริการรันไทม์เพื่อวิเคราะห์โค้ดเป็นแรงผลักดันที่ยิ่งใหญ่ต่อโครงการนั้นใช่
Eric Lippert

108

อัปเดต: เพิ่มเกณฑ์มาตรฐานที่คอมไพล์ไว้ล่วงหน้าและขี้เกียจรวบรวม

อัปเดต 2: ปรากฎว่าฉันคิดผิด ดูโพสต์ของ Eric Lippert สำหรับคำตอบที่สมบูรณ์และถูกต้อง ฉันจะออกจากที่นี่เพื่อประโยชน์ของตัวเลขมาตรฐาน

* อัปเดต 3: เพิ่มเกณฑ์มาตรฐาน IL-Emitted และ Lazy IL ตามคำตอบของ Mark Gravell สำหรับคำถามนี้

ตามความรู้ของฉันการใช้dynamicคีย์เวิร์ดไม่ได้ทำให้เกิดการคอมไพล์เพิ่มเติมใด ๆ ในขณะรันไทม์ในตัวมันเอง (แม้ว่าฉันคิดว่ามันสามารถทำได้ภายใต้สถานการณ์ที่เฉพาะเจาะจงขึ้นอยู่กับประเภทของอ็อบเจ็กต์ที่สำรองตัวแปรไดนามิกของคุณ)

เกี่ยวกับประสิทธิภาพการทำงานdynamicนั้นแนะนำค่าใช้จ่ายบางส่วนโดยเนื้อแท้ แต่ไม่มากเท่าที่คุณคิด ตัวอย่างเช่นฉันเพิ่งใช้เกณฑ์มาตรฐานที่มีลักษณะดังนี้:

void Main()
{
    Foo foo = new Foo();
    var args = new object[0];
    var method = typeof(Foo).GetMethod("DoSomething");
    dynamic dfoo = foo;
    var precompiled = 
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile();
    var lazyCompiled = new Lazy<Action>(() =>
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile(), false);
    var wrapped = Wrap(method);
    var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
    var actions = new[]
    {
        new TimedAction("Direct", () => 
        {
            foo.DoSomething();
        }),
        new TimedAction("Dynamic", () => 
        {
            dfoo.DoSomething();
        }),
        new TimedAction("Reflection", () => 
        {
            method.Invoke(foo, args);
        }),
        new TimedAction("Precompiled", () => 
        {
            precompiled();
        }),
        new TimedAction("LazyCompiled", () => 
        {
            lazyCompiled.Value();
        }),
        new TimedAction("ILEmitted", () => 
        {
            wrapped(foo, null);
        }),
        new TimedAction("LazyILEmitted", () => 
        {
            lazyWrapped.Value(foo, null);
        }),
    };
    TimeActions(1000000, actions);
}

class Foo{
    public void DoSomething(){}
}

static Func<object, object[], object> Wrap(MethodInfo method)
{
    var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
        typeof(object), typeof(object[])
    }, method.DeclaringType, true);
    var il = dm.GetILGenerator();

    if (!method.IsStatic)
    {
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
    }
    var parameters = method.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldelem_Ref);
        il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
    }
    il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
        OpCodes.Call : OpCodes.Callvirt, method, null);
    if (method.ReturnType == null || method.ReturnType == typeof(void))
    {
        il.Emit(OpCodes.Ldnull);
    }
    else if (method.ReturnType.IsValueType)
    {
        il.Emit(OpCodes.Box, method.ReturnType);
    }
    il.Emit(OpCodes.Ret);
    return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}

ดังที่คุณเห็นจากรหัสฉันพยายามเรียกใช้วิธีการไม่ใช้งานง่าย ๆ เจ็ดวิธี:

  1. วิธีการโทรโดยตรง
  2. การใช้ dynamic
  3. โดยการสะท้อน
  4. การใช้Actionที่คอมไพล์ล่วงหน้าในรันไทม์ (ซึ่งไม่รวมเวลาคอมไพล์จากผลลัพธ์)
  5. การใช้Actionที่รวบรวมในครั้งแรกที่จำเป็นโดยใช้ตัวแปร Lazy ที่ไม่ปลอดภัยต่อเธรด (ซึ่งรวมถึงเวลาในการรวบรวม)
  6. ใช้วิธีการสร้างแบบไดนามิกที่สร้างขึ้นก่อนการทดสอบ
  7. ใช้วิธีที่สร้างขึ้นแบบไดนามิกซึ่งจะสร้างอินสแตนซ์อย่างเฉื่อยชาในระหว่างการทดสอบ

แต่ละคนจะถูกเรียก 1 ล้านครั้งในการวนซ้ำง่ายๆ นี่คือผลการจับเวลา:

Direct: 3.4248ms
Dynamic: 45.0728ms
Reflection: 888.4011ms
Precompiled: 21.9166ms
LazyCompiled: 30.2045ms
ILEmitted: 8.4918ms
Lazyilemitted: 14.3483ms

ดังนั้นแม้ว่าการใช้dynamicคีย์เวิร์ดจะใช้ลำดับความสำคัญนานกว่าการเรียกใช้เมธอดโดยตรง แต่ก็ยังสามารถดำเนินการให้เสร็จสมบูรณ์ได้หนึ่งล้านครั้งในเวลาประมาณ 50 มิลลิวินาทีทำให้เร็วกว่าการสะท้อน หากวิธีการที่เราเรียกว่าพยายามทำสิ่งที่เข้มข้นเช่นการรวมสตริงสองสามสายเข้าด้วยกันหรือค้นหาคอลเล็กชันเพื่อหาค่าการดำเนินการเหล่านั้นน่าจะมีค่ามากกว่าความแตกต่างระหว่างการโทรโดยตรงและการdynamicโทร

ประสิทธิภาพเป็นเพียงหนึ่งในหลาย ๆ เหตุผลที่ดีที่จะไม่ใช้dynamicโดยไม่จำเป็น แต่เมื่อคุณจัดการกับdynamicข้อมูลอย่างแท้จริงก็สามารถให้ข้อดีที่มีมากกว่าข้อเสีย

อัปเดต 4

จากความคิดเห็นของ Johnbot ฉันได้แบ่งส่วน Reflection ออกเป็นสี่การทดสอบแยกกัน:

    new TimedAction("Reflection, find method", () => 
    {
        typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
    }),
    new TimedAction("Reflection, predetermined method", () => 
    {
        method.Invoke(foo, args);
    }),
    new TimedAction("Reflection, create a delegate", () => 
    {
        ((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
    }),
    new TimedAction("Reflection, cached delegate", () => 
    {
        methodDelegate.Invoke();
    }),

... และนี่คือผลลัพธ์มาตรฐาน:

ใส่คำอธิบายภาพที่นี่

ดังนั้นหากคุณสามารถกำหนดวิธีการเฉพาะเจาะจงที่คุณจะต้องเรียกใช้เป็นจำนวนมากการเรียกใช้ตัวแทนที่แคชไว้ซึ่งอ้างถึงวิธีการนั้นจะเร็วพอ ๆ กับการเรียกใช้เมธอดนั้นเอง อย่างไรก็ตามหากคุณต้องการกำหนดว่าจะโทรหาวิธีใดเช่นเดียวกับที่คุณกำลังจะเรียกใช้การสร้างผู้รับมอบสิทธิ์จะมีราคาแพงมาก


2
คำตอบโดยละเอียดขอบคุณ! ฉันก็สงสัยเกี่ยวกับตัวเลขที่แท้จริงเช่นกัน
Sergey Sirotkin

4
โค้ดไดนามิกจะเริ่มต้นการนำเข้าข้อมูลเมตาตัววิเคราะห์ความหมายและตัวปล่อยแผนภูมินิพจน์ของคอมไพเลอร์จากนั้นรันคอมไพเลอร์นิพจน์ - ทรี - ทู - อิลบนเอาต์พุตของสิ่งนั้นดังนั้นฉันคิดว่ามันยุติธรรมที่จะบอกว่ามันเริ่มต้น อัพคอมไพเลอร์ที่รันไทม์ เพียงเพราะมันไม่ได้เรียกใช้ lexer และตัวแยกวิเคราะห์แทบจะไม่เกี่ยวข้อง
Eric Lippert

6
ตัวเลขประสิทธิภาพของคุณแสดงให้เห็นว่านโยบายการแคชเชิงรุกของ DLR จ่ายออกไปอย่างไร หากตัวอย่างของคุณทำสิ่งที่น่าขำเช่นหากคุณมีประเภทการรับที่แตกต่างกันทุกครั้งที่โทรคุณจะเห็นว่าเวอร์ชันไดนามิกช้ามากเมื่อไม่สามารถใช้ประโยชน์จากแคชของผลการวิเคราะห์ที่รวบรวมไว้ก่อนหน้านี้ได้ . แต่เมื่อสามารถใช้ประโยชน์จากสิ่งนั้นได้ความดีอันศักดิ์สิทธิ์ก็รวดเร็วตลอดไป
Eric Lippert

1
มีบางอย่างที่น่าเบื่อตามคำแนะนำของ Eric ทดสอบโดยการสลับบรรทัดที่แสดงความคิดเห็น 8964ms vs 814ms dynamicแน่นอนว่าแพ้:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
Brian

1
มีความยุติธรรมในการไตร่ตรองและสร้างผู้รับมอบสิทธิ์จากข้อมูลวิธีการ:var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);
Johnbot
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.