الفضول: لماذا التعبير <...> عند الترجمة تشغيل أسرع من الحد الأدنى DynamicMethod؟

StackOverflow https://stackoverflow.com/questions/1296683

سؤال

أقوم حاليا ببعض التحسينات الأخيرة، في الغالب للمتعة والتعلم، واكتشف شيئا تركني بضع أسئلة.

أولا، الأسئلة:

  1. عندما أقوم بإنشاء طريقة في الذاكرة من خلال استخدام DynamicMethod., واستخدام مصحح الأخطاء، هل هناك أي طريقة بالنسبة لي للتنقل إلى رمز التجميع الذي تم إنشاؤه، عند عرض التعليمات البرمجية في عرض Disassembler؟ يبدو أن مصحح الأخطاء مجرد خطوة على الطريقة بأكملها
  2. أو، إذا لم يكن ذلك ممكنا، هل من الممكن أن نقوم بحفظ رمز IL الذي تم إنشاؤه بطريقة أو بأخرى كمجموعة، حتى أتمكن من فحصه العاكس?
  3. لماذا Expression<...> نسخة من طريقة الإضافة البسيطة الخاصة بي (Int32 + Int32 => Int32) تعمل بشكل أسرع من إصدار DynamicMethod الدنيا؟

إليك برنامج قصير وكامل يوضح. على نظامي، الإخراج هو:

DynamicMethod: 887 ms
Lambda: 1878 ms
Method: 1969 ms
Expression: 681 ms

كنت أتوقع أن تحتوي Lambda وطريقة الاتصال على قيم أعلى، ولكن إصدار DynamicMethod باستمرار حوالي 30-50٪ أبطأ (الاختلافات ربما بسبب Windows والبرامج الأخرى). أي شخص يعرف السبب؟

إليك البرنامج:

using System;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Diagnostics;

namespace Sandbox
{
    public class Program
    {
        public static void Main(String[] args)
        {
            DynamicMethod method = new DynamicMethod("TestMethod",
                typeof(Int32), new Type[] { typeof(Int32), typeof(Int32) });
            var il = method.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Ret);

            Func<Int32, Int32, Int32> f1 =
                (Func<Int32, Int32, Int32>)method.CreateDelegate(
                    typeof(Func<Int32, Int32, Int32>));
            Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
            Func<Int32, Int32, Int32> f3 = Sum;
            Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
            Func<Int32, Int32, Int32> f4 = f4x.Compile();
            for (Int32 pass = 1; pass <= 2; pass++)
            {
                // Pass 1 just runs all the code without writing out anything
                // to avoid JIT overhead influencing the results
                Time(f1, "DynamicMethod", pass);
                Time(f2, "Lambda", pass);
                Time(f3, "Method", pass);
                Time(f4, "Expression", pass);
            }
        }

        private static void Time(Func<Int32, Int32, Int32> fn,
            String name, Int32 pass)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (Int32 index = 0; index <= 100000000; index++)
            {
                Int32 result = fn(index, 1);
            }
            sw.Stop();
            if (pass == 2)
                Debug.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms");
        }

        private static Int32 Sum(Int32 a, Int32 b)
        {
            return a + b;
        }
    }
}
هل كانت مفيدة؟

المحلول

الطريقة التي تم إنشاؤها عبر DynamicMethod يمر من خلال طبعين، في حين أن الطريقة التي تم إنشاؤها عبر Expression<> لا تمر عبر أي.

وإليك كيف يعمل. إليك تسلسل الدعوة للإلغاء fn(0, 1) في ال Time الطريقة (أنا بجد ترميز الحجج إلى 0 و 1 لسهولة التصحيح):

00cc032c 6a01            push    1           // 1 argument
00cc032e 8bcf            mov     ecx,edi
00cc0330 33d2            xor     edx,edx     // 0 argument
00cc0332 8b410c          mov     eax,dword ptr [ecx+0Ch]
00cc0335 8b4904          mov     ecx,dword ptr [ecx+4]
00cc0338 ffd0            call    eax // 1 arg on stack, two in edx, ecx

للحصول على الاحتجاج الأول الذي قمت بالتحقيق فيه، DynamicMethod, ، ال call eax الخط يأتي مثل ذلك:

00cc0338 ffd0            call    eax {003c2084}
0:000> !u 003c2084
Unmanaged code
003c2084 51              push    ecx
003c2085 8bca            mov     ecx,edx
003c2087 8b542408        mov     edx,dword ptr [esp+8]
003c208b 8b442404        mov     eax,dword ptr [esp+4]
003c208f 89442408        mov     dword ptr [esp+8],eax
003c2093 58              pop     eax
003c2094 83c404          add     esp,4
003c2097 83c010          add     eax,10h
003c209a ff20            jmp     dword ptr [eax]

يبدو أن هذا يقوم ببعض المكدس Swizzling لإعادة ترتيب الحجج. أنا مواجهة أنه بسبب الفرق بين المندوبين الذين يستخدمون "هذه" هذه "هذه" وأولئك الذين لا يفعلون ذلك.

أن القفز في النهاية يحل مثل ذلك:

003c209a ff20            jmp     dword ptr [eax]      ds:0023:012f7edc=0098c098
0098c098 e963403500      jmp     00ce0100

يشبه ما تبقى من التعليمات البرمجية في 0098C098 كأنه جيت بريك، الذي حصلت بداية مع jmp بعد الجبهة. إنه فقط بعد أن قفز إلى الرمز الحقيقي:

0:000> !u eip
Normal JIT generated code
DynamicClass.TestMethod(Int32, Int32)
Begin 00ce0100, size 5
>>> 00ce0100 03ca            add     ecx,edx
00ce0102 8bc1            mov     eax,ecx
00ce0104 c3              ret

تسلسل الاحتجاج للطريقة التي تم إنشاؤها عبر Expression<> يختلف - إنه في عداد المفقودين رمز Suizzling Stack. هنا هو، من القفز الأول عبر eax:

00cc0338 ffd0            call    eax {00ce00a8}

0:000> !u eip
Normal JIT generated code
DynamicClass.lambda_method(System.Runtime.CompilerServices.ExecutionScope, Int32, Int32)
Begin 00ce00a8, size b
>>> 00ce00a8 8b442404        mov     eax,dword ptr [esp+4]
00ce00ac 03d0            add     edx,eax
00ce00ae 8bc2            mov     eax,edx
00ce00b0 c20400          ret     4

الآن، كيف تحصل الأمور مثل هذا؟

  1. لم يكن Swizzling Stack ضروريا (يتم استخدام الوسيطة الأولى الضمنية من المندوب بالفعل، أي ليس مثل مندوب ملزم بطريقة ثابتة)
  2. يجب أن يكون الجيت قد أجبر منطق تجميع لينك بحيث عقد المندوب عنوان الوجهة الحقيقية بدلا من واحد مزيف.

لا أعرف كيف أجبرت Linq Jit، لكنني أعرف كيفية إجبار JIT بنفسي - عن طريق استدعاء الوظيفة مرة واحدة على الأقل. تحديث: لقد وجدت وسيلة أخرى لإجبار JIT: استخدم restrictedSkipVisibility Argumetn إلى المنشئ وتمرير true. وبعد لذلك، إليك الرمز المعدل الذي يزيل كومة Swizzling باستخدام المعلمة الضمنية "هذه" الضمنية، ويستخدم المنشئ البديل إلى الترجمة مسبقا بحيث يكون العنوان المنضم هو العنوان الحقيقي، بدلا من Thunk:

using System;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Diagnostics;

namespace Sandbox
{
    public class Program
    {
        public static void Main(String[] args)
        {
            DynamicMethod method = new DynamicMethod("TestMethod",
                typeof(Int32), new Type[] { typeof(object), typeof(Int32),
                typeof(Int32) }, true);
            var il = method.GetILGenerator();

            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Ldarg_2);
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Ret);

            Func<Int32, Int32, Int32> f1 =
                (Func<Int32, Int32, Int32>)method.CreateDelegate(
                    typeof(Func<Int32, Int32, Int32>), null);
            Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
            Func<Int32, Int32, Int32> f3 = Sum;
            Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
            Func<Int32, Int32, Int32> f4 = f4x.Compile();
            for (Int32 pass = 1; pass <= 2; pass++)
            {
                // Pass 1 just runs all the code without writing out anything
                // to avoid JIT overhead influencing the results
                Time(f1, "DynamicMethod", pass);
                Time(f2, "Lambda", pass);
                Time(f3, "Method", pass);
                Time(f4, "Expression", pass);
            }
        }

        private static void Time(Func<Int32, Int32, Int32> fn,
            String name, Int32 pass)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (Int32 index = 0; index <= 100000000; index++)
            {
                Int32 result = fn(index, 1);
            }
            sw.Stop();
            if (pass == 2)
                Console.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms");
        }

        private static Int32 Sum(Int32 a, Int32 b)
        {
            return a + b;
        }
    }
}

إليك Runtimes في نظامي:

DynamicMethod: 312 ms
Lambda: 417 ms
Method: 417 ms
Expression: 312 ms

تحديث لإضافة:

حاولت تشغيل هذا الرمز على نظامي الجديد، وهو تشغيل Windows 7 X64 يعمل بنظام Windows 7 X64 مع .NET 4 Beta 2 مثبتة (mscoree.dll ver. 4.0.30902)، والنتائج هي، حسنا، متغير.

csc 3.5, /platform:x86, runtime v2.0.50727 (via .config)

Run #1
DynamicMethod: 214 ms
Lambda: 571 ms
Method: 570 ms
Expression: 249 ms

Run #2
DynamicMethod: 463 ms
Lambda: 392 ms
Method: 392 ms
Expression: 463 ms

Run #3
DynamicMethod: 463 ms
Lambda: 570 ms
Method: 570 ms
Expression: 463 ms

ربما هذا هو Intel Speedstep الذي يؤثر على النتائج، أو ربما دفعة توربو. في أي حال، إنه مزعج للغاية.

csc 3.5, /platform:x64, runtime v2.0.50727 (via .config)
DynamicMethod: 428 ms
Lambda: 392 ms
Method: 392 ms
Expression: 428 ms

csc 3.5, /platform:x64, runtime v4
DynamicMethod: 428 ms
Lambda: 356 ms
Method: 356 ms
Expression: 428 ms

csc 4, /platform:x64, runtime v4
DynamicMethod: 428 ms
Lambda: 356 ms
Method: 356 ms
Expression: 428 ms

csc 4, /platform:x86, runtime v4
DynamicMethod: 463 ms
Lambda: 570 ms
Method: 570 ms
Expression: 463 ms

csc 3.5, /platform:x86, runtime v4
DynamicMethod: 214 ms
Lambda: 570 ms
Method: 571 ms
Expression: 249 ms

ستكون العديد من هذه النتائج حوادث توقيت، مهما كانت تسبب تسريع عشوائي في سيناريو C # 3.5 / Runtime V2.0. سأضطر إلى إعادة التشغيل لمعرفة ما إذا كانت SpeedStep أو Borbo Boost هي المسؤولة عن هذه الآثار.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top