.NET 5 के लिए JIT कंपाइलर ऑप्टिमाइज़ेशन

कुछ समय पहले, मैंने जेआईटी कंपाइलर की दुनिया में एक अद्भुत यात्रा शुरू की ताकि आप ऐसी जगहों का पता लगा सकें जहाँ आप अपने हाथों को थाम सकते हैं और कुछ को गति दे सकते हैं, जैसे कि मुख्य कार्य के दौरान, एलएलवीएम और इसके अनुकूलन में थोड़ी मात्रा में ज्ञान जमा हुआ है। इस लेख में, मैं JIT में अपने सुधारों की एक सूची साझा करना चाहूंगा (.NET में इसे कुछ ड्रैगन या एनीमे के सम्मान में RyuJIT कहा जाता है - मैंने इसका पता नहीं लगाया), जिनमें से अधिकांश पहले ही मास्टर तक पहुंच चुके हैं और .NET (कोर) 5 में उपलब्ध होंगे मेरी आशाएँ जेआईटी के विभिन्न चरणों को प्रभावित करती हैं, जिन्हें बहुत ही योजनाबद्ध रूप से निम्न प्रकार से दिखाया जा सकता है:



जैसा कि आरेख से देखा जा सकता है, जेआईटी संकीर्ण जेट-इंटरफेस से संबंधित एक अलग मॉड्यूल है , जिसके द्वारा जेआईटी कुछ चीजों पर सहमति देता है, उदाहरण के लिए, क्या यह संभव है।एक वर्ग को दूसरे में डाला। बाद में JIT Tier1 में विधि संकलित करता है, रनटाइम जितनी अधिक जानकारी प्रदान कर सकता है, उदाहरण के लिए, कि static readonlyखेतों को एक स्थिर से बदला जा सकता है, क्योंकि वर्ग पहले से ही वैधानिक रूप से आरंभिक है।

तो, चलो सूची के साथ शुरू करते हैं।

PR # 1817 : पैटर्न मिलान में बॉक्सिंग / अनबॉक्सिंग अनुकूलन


चरण: आयातक
नई सी # सुविधाओं में से कई अक्सर बॉक्स / अनबॉक्स सीआईएल ऑपकोड डालकर पाप करते हैं यह एक बहुत महंगा ऑपरेशन है, जो अनिवार्य रूप से ढेर पर एक नई वस्तु का आवंटन है, इसमें स्टैक से मूल्य की प्रतिलिपि बनाता है, और फिर अंत में जीसी को भी लोड करता है। इस मामले में JIT में पहले से ही कई अनुकूलन हैं, लेकिन मुझे उदाहरण के लिए C # 8 में मिसिंग पैटर्न मिला,:

public static int Case1<T>(T o)
{
    if (o is int x)
        return x;
    return 0;
}

public static int Case2<T>(T o) => o is int n ? n : 42;

public static int Case3<T>(T o)
{
    return o switch
    {
        int n => n,
        string str => str.Length,
        _ => 0
    };
}

और आइए देखें कि मेरे अनुकूलन से पहले asm-codegen (उदाहरण के लिए, int विशेषज्ञता के लिए) सभी तीन तरीकों के लिए:



और अब मेरे सुधार के बाद:



तथ्य यह है कि अनुकूलन में IL कोड के पैटर्न मिले हैं

box !!T
isinst Type1
unbox.any Type2

आयात करने और प्रकारों के बारे में जानकारी रखने के बाद, मैं केवल इन ऑपकोड को अनदेखा करने और बॉक्सिंग-इनबॉक्स डालने में सक्षम नहीं था। वैसे, मैंने मोनो में भी एक ही अनुकूलन लागू किया हैइसके बाद, पुल-अनुरोध का एक लिंक अनुकूलन विवरण के हेडर में निहित है।

PR # 1157 टाइपोफ़ (T) .IsValueType false सही / गलत


चरण: आयातक
यहां मैंने JIT को तुरंत टाइप करने के लिए Type.IsValueType की जगह यदि संभव हो तो प्रशिक्षित किया यह एक चुनौती है और भविष्य में पूरी स्थितियों और शाखाओं को काटने की क्षमता है, एक उदाहरण:

void Foo<T>()
{
    if (!typeof(T).IsValueType)
        Console.WriteLine("not a valuetype");
}

और चलो सुधार से पहले फू <int> विशेषज्ञता के लिए कोडजेन देखें :



और सुधार के बाद:



यदि आवश्यक हो तो अन्य प्रकार के गुणों के साथ भी ऐसा किया जा सकता है।

पीआर # 1157 typeof(T1).IsAssignableFrom(typeof(T2)) ⇨ true/false


चरण: आयातक
लगभग एक ही बात - अब आप बिना किसी डर के सामान्य तरीकों में पदानुक्रम की जांच कर सकते हैं कि यह अनुकूलित नहीं है, उदाहरण के लिए:

void Foo<T1, T2>()
{
    if (!typeof(T1).IsAssignableFrom(typeof(T2)))
        Console.WriteLine("T1 is not assignable from T2");
}

उसी तरह, यह एक निरंतर द्वारा प्रतिस्थापित किया जाएगा true/falseऔर पूरी तरह से स्थिति को हटाया जा सकता है। इस तरह के अनुकूलन में, निश्चित रूप से, कुछ कोने के मामले हैं जिन्हें आपको हमेशा ध्यान में रखना चाहिए: सिस्टम .__ कैनन ने साझा जेनेरिक, सरणियां, सह (ntr) परिवर्तनशीलता, अशक्तियां, COM ऑब्जेक्ट्स, आदि।

पीआर # 1378 "Hello".Length ⇨ 5


चरण: आयातक
इस तथ्य के बावजूद कि अनुकूलन जितना संभव हो उतना स्पष्ट और सरल है, मुझे जेआईटी-ई में इसे लागू करने के लिए बहुत पसीना बहाना पड़ा। बात यह है कि जेआईटी को स्ट्रिंग की सामग्री के बारे में नहीं पता था, उन्होंने स्ट्रिंग लिटरल्स ( GT_CNS_STR ) को देखा , लेकिन स्ट्रिंग्स की विशिष्ट सामग्री के बारे में कुछ भी नहीं पता था। मुझे वीएम से संपर्क करके (पूर्वोक्त जेआईटी-इंटरफ़ेस का विस्तार करने के लिए) मदद करनी थी, और स्वयं अनुकूलन अनिवार्य रूप से कोड की कुछ पंक्तियाँ हैं । ऐसे बहुत से उपयोगकर्ता मामले हैं, स्पष्ट लोगों के अलावा, जैसे: str.IndexOf("foo") + "foo".Lengthगैर-स्पष्ट वाले जिनमें इनलाइनिंग शामिल है (मैं आपको याद दिलाता हूं: रोजलिन इनलाइनिंग से निपटता नहीं है, इसलिए यह अनुकूलन इसमें अप्रभावी होगा, जैसे सब कुछ), एक उदाहरण:

bool Validate(string str) => str.Length > 0 && str.Length <= 100;

bool Test() => Validate("Hello");

के लिए codegen पर आइए नज़र टेस्ट ( मान्य है : इनलाइन)



और अब codegen अनुकूलन जोड़ने के बाद:



अर्थात इनलाइन विधि, स्ट्रिंग वेरिएबल के साथ वैरिएबल को बदलें, बदलें। वास्तविक स्ट्रिंग लेंथ के साथ शाब्दिक से गति, स्थिरांक को मोड़ें, मृत कोड हटाएं। वैसे, चूंकि जेआईटी अब एक स्ट्रिंग की सामग्री की जांच कर सकती है, स्ट्रिंग शाब्दिक से संबंधित अन्य अनुकूलन के लिए दरवाजे खुल गए हैं। अनुकूलन का उल्लेख केवल .NET 5.0 के पहले पूर्वावलोकन की घोषणा में किया गया था: devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-1 अनुभाग कोड गुणवत्ता सुधार में RyuJIT में

PR # 1644: बाउंड चेक को ऑप्टिमाइज़ करना।


चरण:
कई के लिए सीमाएं जाँच समाप्त करें , यह एक रहस्य नहीं होगा कि हर बार जब आप किसी सरणी को इंडेक्स द्वारा एक्सेस करते हैं, तो JIT आपके लिए एक चेक सम्मिलित करता है कि सरणी आगे नहीं जाती है और ऐसा होने पर अपवाद को फेंक देता है - गलत तर्क के मामले में, आप नहीं कर सके यादृच्छिक स्मृति पढ़ने के लिए, कुछ मूल्य प्राप्त करें और जारी रखें।

int Foo(int[] array, int index)
{
    // if ((uint) array.Length <= (uint) index)
    //     throw new IndexOutOfRangeException();
    return array[index];
}

ऐसा एक चेक उपयोगी है, लेकिन यह प्रदर्शन को बहुत प्रभावित कर सकता है: सबसे पहले, यह एक तुलनात्मक ऑपरेशन जोड़ता है और आपके कोड को असंबद्ध बनाता है, और दूसरी बात, यह सभी परिणामों के साथ आपकी विधि में एक अपवाद कॉलिंग कोड जोड़ता है। हालांकि, कई मामलों में, जेआईटी इन चेक को हटा सकता है यदि यह साबित कर सकता है कि सूचकांक कभी भी इससे आगे नहीं बढ़ेगा, या यह कि पहले से ही कुछ अन्य चेक है और आपको एक और जोड़ने की आवश्यकता नहीं है - सीमा (सीमा) चेक उन्मूलन। मुझे कई ऐसे मामले मिले, जिनमें वह उनका सामना नहीं कर पाया और उन्हें सुधार सका (और भविष्य में मैं इस चरण के कुछ और सुधारों की योजना बना रहा हूं)।

var item = array[index & mask];

यहां इस कोड में, मैं जेआईटी को बताता हूं कि & maskअनिवार्य रूप से ऊपर से मूल्य तक सूचकांक को सीमित करता है mask, अर्थात। यदि maskसरणी का मान और लंबाई JIT के लिए जानी जाती है , तो आप एक बाउंड चेक नहीं डाल सकते। वही%, (और x >> y) परिचालनों के लिए जाता है। Aspnetcore में इस अनुकूलन का उपयोग करने का एक उदाहरण
इसके अलावा, यदि हम जानते हैं कि हमारे सरणी में, उदाहरण के लिए, 256 तत्व या अधिक हैं, तो यदि हमारा अज्ञात इंडेक्स बाइट प्रकार का है, तो यह कितना भी कठिन प्रयास करे, यह कभी भी सीमा से बाहर नहीं निकल पाएगा। PR: github.com/dotnet/coreclr/pull/25912

पीआर # 24584: x / 2 ⇨ x * 0.5


चरण:
इस पीआर के मॉर्फ सी और जेआईटी ऑप्टिमाइज़ेशन की दुनिया में अपना अद्भुत गोता लगाने लगे। ऑपरेशन "डिवीजन" ऑपरेशन "गुणा" की तुलना में धीमा है (और यदि पूर्णांकों के लिए और सामान्य रूप से - परिमाण का एक क्रम)। केवल दो की शक्ति के बराबर स्थिरांक के लिए काम करता है, उदाहरण:

static float DivideBy2(float x) => x / 2; // = x * 0.5; 

अनुकूलन से पहले कोडगैन:



और उसके बाद:



यदि हम हसवेल के इन दो निर्देशों की तुलना करते हैं, तो सब कुछ स्पष्ट हो जाएगा:

vdivss (Latency: 10-20,  R.Throughput: 7-14)
vmulss (Latency:     5,  R.Throughput:  0.5)

इसके बाद वे अनुकूलन होंगे जो अभी भी कोड-समीक्षा के चरण में हैं और इस तथ्य पर नहीं कि उन्हें स्वीकार किया जाएगा।

पीआर # 31978: Math.Pow(x, 2) ⇨ x * x


चरण: आयातक
सब कुछ यहाँ सरल है: बजाय एक लोकप्रिय मामले के लिए पॉव (एफ) को कॉल करने के बजाय, जब डिग्री लगातार 2 (अच्छी तरह से, यह 1, -1, 0 के लिए मुफ्त है), तो आप इसे एक साधारण x * x में विस्तारित कर सकते हैं। आप किसी भी अन्य डिग्री का विस्तार कर सकते हैं, लेकिन इसके लिए आपको .NET में "तेज गणित" मोड के कार्यान्वयन के लिए इंतजार करना होगा, जिसमें प्रदर्शन के लिए IEEE-754 विनिर्देश को उपेक्षित किया जा सकता है। उदाहरण:

static float Pow2(float x) => MathF.Pow(x, 2);

अनुकूलन से पहले कोडगन:



और उसके बाद:



पीआर # 33024: x * 2 ⇨ x + x


चरण: कम
इसके अलावा काफी सरल सूक्ष्म (नैनो) piphol अनुकूलन, रजिस्टर में निरंतर लोड किए बिना आप 2 से गुणा करने के लिए अनुमति देता है।

static float MultiplyBy2(float x) => x * 2;

अनुकूलन से पहले कोडगेन: के



बाद:



सामान्य तौर पर, निर्देश mul(ss/sd/ps/pd)विलंबता और प्रवाह में समान है add(ss/sd/ps/pd), लेकिन निरंतर "2" लोड करने की आवश्यकता काम को थोड़ा धीमा कर सकती है। यहां, ऊपर दिए गए कोडजेन के उदाहरण में, मैंने vaddssएक रजिस्टर के ढांचे के भीतर सब कुछ किया।

PR # 32368: Array.Length / c (या% s) का अनुकूलन


चरण: मोर्फ
यह सिर्फ इतना हुआ कि ऐरे की लंबाई क्षेत्र एक हस्ताक्षरित प्रकार है, और एक निरंतर द्वारा विभाजन और शेष एक अहस्ताक्षरित प्रकार से करने के लिए बहुत अधिक कुशल हैं (और सिर्फ दो की शक्ति नहीं), बस इस कोड की तुलना करें:



मेरा पीआर JIT की याद दिलाता है कि Array.Lengthहालांकि महत्वपूर्ण है, लेकिन वास्तव में, सरणी की लंबाई कभी भी ( जब तक कि आप अराजकतावादी नहीं होते हैं ) शून्य से कम हो सकती है, जिसका अर्थ है कि आप इसे एक अहस्ताक्षरित संख्या के रूप में देख सकते हैं और यूइंट के लिए कुछ अनुकूलन जैसे लागू कर सकते हैं।

PR # 32716: शाखा रहित कोड में सरल तुलना का अनुकूलन


चरण: प्रवाह विश्लेषण
यह अनुकूलन का एक और वर्ग है जो एक के भीतर अभिव्यक्ति के बजाय बुनियादी ब्लॉकों से संचालित होता है। यहां जेआईटी थोड़ा रूढ़िवादी है और इसमें सुधार के लिए जगह है, उदाहरण के लिए जहां संभव हो वहां cmove आवेषण। मैंने इस मामले के लिए एक सरल अनुकूलन के साथ शुरुआत की:

x = condition ? A : B;

यदि A और B स्थिरांक हैं और उनके बीच का अंतर एकता है, उदाहरण के लिए, condition ? 1 : 2तो हम, यह जानते हुए कि तुलनात्मक संचालन अपने आप में 0 या 1 देता है, कूद को ऐड से बदल सकता है। RyuJIT के संदर्भ में, यह कुछ इस तरह दिखता है: मैं खुद पीआर



के विवरण को देखने की सलाह देता हूं, मुझे उम्मीद है कि वहां सब कुछ स्पष्ट रूप से वर्णित है।

सभी अनुकूलन समान रूप से उपयोगी नहीं हैं।


ऑप्टिमाइज़ेशन के लिए उच्च शुल्क की आवश्यकता होती है:
* समर्थन और पढ़ने के लिए मौजूदा कोड की जटिलता = वृद्धि
* संभावित बग: परीक्षण कंपाइलर अनुकूलन कुछ भी याद करने और उपयोगकर्ताओं से किसी प्रकार का segfault प्राप्त करने के लिए बेहद मुश्किल और आसान है।
* धीमा संकलन
* जेआईटी बिनार का आकार बढ़ाना

जैसा कि आप पहले से ही समझ चुके हैं, अनुकूलन के सभी विचारों और प्रोटोटाइप को स्वीकार नहीं किया जाता है और यह साबित करना आवश्यक है कि उन्हें जीवन का अधिकार है। .NET में इसे साबित करने के लिए स्वीकृत तरीकों में से एक है- jit-utils उपयोगिता को चलाना, जो AOT पुस्तकालयों के सेट (सभी BCL और corelib) को संकलित करेगा और अनुकूलन से पहले और बाद के सभी तरीकों के लिए कोडांतरक कोड की तुलना करेगा, यह है कि यह रिपोर्ट अनुकूलन के लिए कैसा दिखता है"str".Lengthरिपोर्ट के अलावा, अभी भी लोगों का एक निश्चित चक्र है (जैसे कि jkotas ), जो एक नज़र में, उपयोगिता का मूल्यांकन कर सकते हैं और अपने अनुभव की ऊंचाई से सब कुछ हैक कर सकते हैं और समझ सकते हैं कि .NET में कौन से स्थान एक अड़चन हो सकते हैं और कौन से नहीं। और एक और बात: "कोई नहीं लिखता है" इस तर्क से अनुकूलन का न्याय न करें, "रोजलिन में चेतावनी देना बेहतर होगा" - आपको यह कभी नहीं पता होगा कि जेआईटी के आने के बाद आपका कोड कैसे दिखेगा जो कि संभव है और निरंतरता में भरता है।

All Articles