Làm thế nào để có một biến động ảnh hưởng đến hiệu suất?


127

Tôi có một câu hỏi về hiệu suất của dynamicC #. Tôi đã đọc dynamiclàm cho trình biên dịch chạy lại, nhưng nó làm gì?

Liệu nó có phải biên dịch lại toàn bộ phương thức với dynamicbiến được sử dụng làm tham số hay chỉ những dòng có hành vi / ngữ cảnh động?

Tôi đã nhận thấy rằng việc sử dụng dynamiccác biến có thể làm chậm một vòng lặp đơn giản với 2 bậc độ lớn.

Mã tôi đã chơi với:

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

Không, nó không chạy trình biên dịch, điều đó sẽ khiến nó bị phạt chậm trong lần đầu tiên. Khá giống với Reflection nhưng với rất nhiều thông minh để theo dõi những gì đã được thực hiện trước đó để giảm thiểu chi phí. Google "thời gian chạy ngôn ngữ động" để hiểu rõ hơn. Và không, nó sẽ không bao giờ đạt đến tốc độ của vòng lặp 'bản địa'.
Hans Passant

Câu trả lời:


232

Tôi đã đọc động làm cho trình biên dịch chạy lại, nhưng những gì nó làm. Liệu nó có phải biên dịch lại toàn bộ phương thức với động được sử dụng làm tham số hay đúng hơn là các dòng có hành vi / ngữ cảnh động (?)

Đây là thỏa thuận.

Đối với mọi biểu thức trong chương trình của bạn thuộc loại động, trình biên dịch sẽ phát ra mã tạo ra một "đối tượng trang web cuộc gọi động" duy nhất đại diện cho hoạt động. Vì vậy, ví dụ, nếu bạn có:

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

sau đó trình biên dịch sẽ tạo mã giống như thế này. (Mã thực tế phức tạp hơn một chút; điều này được đơn giản hóa cho mục đích trình bày.)

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

Xem cách này hoạt động cho đến nay? Chúng tôi tạo trang web cuộc gọi một lần , bất kể bạn gọi M. bao nhiêu lần Trang web cuộc gọi tồn tại mãi mãi sau khi bạn tạo một lần. Trang web cuộc gọi là một đối tượng thể hiện "sẽ có một cuộc gọi động đến Foo tại đây".

OK, vậy bây giờ bạn đã có trang web cuộc gọi, cách gọi hoạt động như thế nào?

Trang web cuộc gọi là một phần của Thời gian chạy ngôn ngữ động. DLR nói "hmm, ai đó đang cố thực hiện một lệnh gọi động của một phương thức foo trên đối tượng này ở đây. Tôi có biết gì về điều đó không? Không. Vậy thì tốt hơn tôi nên tìm hiểu."

DLR sau đó thẩm vấn đối tượng trong d1 để xem nó có gì đặc biệt không. Có thể đó là một đối tượng COM kế thừa hoặc đối tượng Iron Python hoặc đối tượng Iron Ruby hoặc đối tượng IE DOM. Nếu nó không phải là một trong số đó thì nó phải là một đối tượng C # bình thường.

Đây là điểm mà trình biên dịch khởi động lại. Không cần trình phân tích cú pháp hoặc trình phân tích cú pháp, vì vậy DLR khởi động phiên bản đặc biệt của trình biên dịch C # chỉ có trình phân tích siêu dữ liệu, trình phân tích ngữ nghĩa cho các biểu thức và trình phát phát ra Cây biểu thức thay vì IL.

Trình phân tích siêu dữ liệu sử dụng Reflection để xác định loại đối tượng trong d1 và sau đó chuyển nó đến bộ phân tích ngữ nghĩa để hỏi điều gì xảy ra khi một đối tượng như vậy được gọi trên phương thức Foo. Trình phân tích độ phân giải quá tải chỉ ra rằng, sau đó xây dựng Cây biểu thức - giống như khi bạn gọi Foo trong cây biểu thức lambda - đại diện cho lệnh gọi đó.

Trình biên dịch C # sau đó chuyển cây biểu thức đó trở lại DLR cùng với chính sách bộ đệm. Chính sách này thường là "lần thứ hai bạn nhìn thấy một đối tượng thuộc loại này, bạn có thể sử dụng lại cây biểu thức này thay vì gọi lại cho tôi". Sau đó, DLR gọi Compile trên cây biểu thức, gọi trình biên dịch biểu thức cây-to-IL và tạo ra một khối IL được tạo động trong một ủy nhiệm.

DLR sau đó lưu trữ ủy nhiệm này trong bộ đệm được liên kết với đối tượng trang gọi.

Sau đó, nó gọi đại biểu và cuộc gọi Foo xảy ra.

Lần thứ hai bạn gọi M, chúng tôi đã có một trang web cuộc gọi. DLR thẩm vấn đối tượng một lần nữa và nếu đối tượng đó cùng loại với lần trước, nó sẽ lấy đại biểu ra khỏi bộ đệm và gọi nó. Nếu đối tượng thuộc loại khác thì bộ đệm sẽ bỏ lỡ và toàn bộ quá trình bắt đầu lại; chúng tôi phân tích ngữ nghĩa của cuộc gọi và lưu trữ kết quả vào bộ đệm.

Điều này xảy ra cho mọi biểu hiện liên quan đến năng động. Vì vậy, ví dụ nếu bạn có:

int x = d1.Foo() + d2;

sau đó có ba trang web cuộc gọi động. Một cho cuộc gọi động đến Foo, một cho bổ sung động và một cho chuyển đổi động từ động sang int. Mỗi người có phân tích thời gian chạy riêng và bộ đệm riêng cho kết quả phân tích.

Có lý?


Vì tò mò, phiên bản trình biên dịch đặc biệt không có trình phân tích cú pháp / lexer được gọi bằng cách chuyển một cờ đặc biệt đến csc.exe tiêu chuẩn?
Roman Royter

@Eric, tôi có thể gặp rắc rối khi bạn chỉ cho tôi một bài đăng trên blog trước đây của bạn không, nơi bạn nói về chuyển đổi ngầm của short, int, v.v.? Như tôi nhớ bạn đã đề cập trong đó làm thế nào / tại sao sử dụng động với Convert.ToXXX làm cho trình biên dịch khởi động. Tôi chắc chắn rằng tôi đang xem chi tiết, nhưng hy vọng bạn biết tôi đang nói về cái gì.
Adam Rackis

4
@Roman: Số csc.exe được viết bằng C ++ và chúng tôi cần một cái gì đó chúng tôi có thể dễ dàng gọi từ C #. Ngoài ra, trình biên dịch dòng chính có các đối tượng kiểu riêng, nhưng chúng ta cần có khả năng sử dụng các đối tượng kiểu Reflection. Chúng tôi đã trích xuất các phần có liên quan của mã C ++ từ trình biên dịch csc.exe và dịch chúng từng dòng thành C #, sau đó xây dựng một thư viện từ đó để DLR gọi.
Eric Lippert

9
@Eric, "Chúng tôi đã trích xuất các phần có liên quan của mã C ++ từ trình biên dịch csc.exe và dịch chúng từng dòng thành C #" là lúc đó mọi người nghĩ Roslyn có thể đáng để theo đuổi :)
ShuggyCoUk

5
@ShuggyCoUk: Ý tưởng về việc dịch vụ biên dịch đã được sử dụng một thời gian, nhưng thực sự cần một dịch vụ thời gian chạy để phân tích mã là một động lực lớn đối với dự án đó, vâng.
Eric Lippert

107

Cập nhật: Đã thêm các điểm chuẩn được biên dịch sẵn và lười biên dịch

Cập nhật 2: Hóa ra, tôi sai. Xem bài của Eric Lippert để có câu trả lời đầy đủ và chính xác. Tôi rời khỏi đây vì lợi ích của các số điểm chuẩn

* Cập nhật 3: Đã thêm các điểm chuẩn IL-Emited và Lazy IL-Emited, dựa trên câu trả lời của Mark Gravell cho câu hỏi này .

Theo hiểu biết của tôi, việc sử dụng dynamictừ khóa không gây ra bất kỳ sự biên dịch bổ sung nào trong thời gian chạy (mặc dù tôi tưởng tượng nó có thể làm như vậy trong các trường hợp cụ thể, tùy thuộc vào loại đối tượng nào đang hỗ trợ các biến động của bạn).

Về hiệu suất, dynamicvốn đã giới thiệu một số chi phí, nhưng không nhiều như bạn nghĩ. Ví dụ, tôi vừa chạy một điểm chuẩn trông như thế này:

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

Như bạn có thể thấy từ mã, tôi cố gắng gọi một phương thức no-op đơn giản theo bảy cách khác nhau:

  1. Gọi phương thức trực tiếp
  2. Sử dụng dynamic
  3. Theo phản xạ
  4. Sử dụng một Actioncái đã được biên dịch trước trong thời gian chạy (do đó không bao gồm thời gian biên dịch từ các kết quả).
  5. Sử dụng một Actioncái được biên dịch lần đầu tiên là cần thiết, sử dụng biến Lazy không an toàn cho luồng (do đó bao gồm cả thời gian biên dịch)
  6. Sử dụng phương pháp tạo động được tạo trước khi kiểm tra.
  7. Sử dụng một phương pháp được tạo động mà ngay lập tức bị lười biếng trong quá trình thử nghiệm.

Mỗi lần được gọi là 1 triệu lần trong một vòng lặp đơn giản. Dưới đây là kết quả thời gian:

Trực tiếp: 3,4248ms
Năng động: 45,0728ms
Phản xạ: 888.4011ms Được
biên dịch trước: 21.9166ms
LazyCompiled: 30.2045ms
ILEmit: 8.4918ms
LazyILEmit: 14.3483ms

Vì vậy, trong khi sử dụng dynamictừ khóa mất nhiều thời gian hơn so với gọi trực tiếp phương thức, nó vẫn quản lý để hoàn thành thao tác một triệu lần trong khoảng 50 mili giây, làm cho nó nhanh hơn nhiều so với phản xạ. Nếu phương thức chúng ta gọi là cố gắng thực hiện một cái gì đó chuyên sâu, như kết hợp một vài chuỗi với nhau hoặc tìm kiếm một bộ sưu tập cho một giá trị, thì các thao tác đó có thể vượt xa sự khác biệt giữa cuộc gọi trực tiếp và dynamiccuộc gọi.

Hiệu suất chỉ là một trong nhiều lý do tốt để không sử dụng một cách dynamickhông cần thiết, nhưng khi bạn xử lý dynamicdữ liệu thực sự , nó có thể cung cấp các lợi thế vượt xa các nhược điểm.

Cập nhật 4

Dựa trên nhận xét của Johnbot, tôi đã chia khu vực Reflection thành bốn bài kiểm tra riêng biệt:

    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();
    }),

... và đây là kết quả điểm chuẩn:

nhập mô tả hình ảnh ở đây

Vì vậy, nếu bạn có thể xác định trước một phương thức cụ thể mà bạn sẽ cần gọi rất nhiều, việc gọi một đại biểu được lưu trong bộ nhớ cache tham chiếu đến phương thức đó cũng nhanh như gọi chính phương thức đó. Tuy nhiên, nếu bạn cần xác định phương thức nào để gọi giống như bạn sắp gọi nó, thì việc tạo một đại biểu cho nó rất tốn kém.


2
Như một phản ứng chi tiết, cảm ơn! Tôi đã tự hỏi về những con số thực tế là tốt.
Serge Sirotkin

4
Chà, mã động khởi động trình nhập siêu dữ liệu, trình phân tích ngữ nghĩa và trình phát cây biểu thức của trình biên dịch, rồi chạy trình biên dịch biểu thức cây-to-il trên đầu ra của nó, vì vậy tôi nghĩ rằng thật công bằng khi nói rằng nó khởi động lên trình biên dịch khi chạy. Chỉ vì nó không chạy lexer và trình phân tích cú pháp dường như không liên quan.
Eric Lippert

6
Số hiệu suất của bạn chắc chắn cho thấy chính sách bộ nhớ đệm tích cực của DLR được đền đáp như thế nào. Nếu ví dụ của bạn đã làm những điều ngớ ngẩn, chẳng hạn như nếu bạn có một loại nhận khác nhau mỗi khi bạn thực hiện cuộc gọi, thì bạn sẽ thấy rằng phiên bản động rất chậm khi không thể tận dụng bộ đệm của kết quả phân tích được biên dịch trước đó . Nhưng khi nó có thể tận dụng điều đó, lòng tốt thánh thiện sẽ nhanh chóng.
Eric Lippert

1
Một cái gì đó ngớ ngẩn theo đề nghị của Eric. Kiểm tra bằng cách hoán đổi dòng nào được nhận xét. 8964ms so với 814ms, dynamictất nhiên là thua: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
Hãy công bằng để phản ánh và tạo một đại biểu từ thông tin phương pháp:var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);
Johnbot
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.