Golang: مشاكل الأداء المحددة

لغة Go تكتسب شعبية. واثق من أن هناك المزيد والمزيد من المؤتمرات ، على سبيل المثال ، GolangConf ، واللغة هي من بين التقنيات العشرة الأكثر ربحًا . لذلك ، من المنطقي بالفعل التحدث عن مشكلاتها المحددة ، على سبيل المثال ، الأداء. بالإضافة إلى المشاكل الشائعة لجميع اللغات المترجمة ، فإن Go لها قناعاتها. ترتبط مع المحسن ، المكدس ، نظام الكتابة ، ونموذج تعدد المهام. طرق لحلها والحلول في بعض الأحيان محددة للغاية.

دانييل بودولسكي ، على الرغم من مبشر جو ، يواجه أيضًا الكثير من الأشياء الغريبة فيه. كل شيء غريب ، والأهم من ذلك ، مثير للاهتمام ، يجمع ويختبر ، ثم يتحدث عن ذلك في HighLoad ++. سيتضمن نص التقرير الأرقام والرسوم البيانية وأمثلة الكود ونتائج المحلل ، ومقارنة أداء الخوارزميات نفسها بلغات مختلفة - وكل شيء آخر ، نكره كلمة "التحسين". لن يكون هناك أي كشف في النص - من أين أتوا بهذه اللغة البسيطة - وكل ما يمكن قراءته في الصحف.



عن المتحدثين. دانييل بودولسكي : 26 عامًا من الخبرة ، 20 عامًا في العملية ، بما في ذلك قائد المجموعة ، 5 سنوات من البرمجة على Go. كيريل دانشين: مبدع Gramework ، Maintainer ، Fast HTTP ، Black Go-mage.

قام بإعداد التقرير دانييل بودولسكي وكيريل دانشين ، لكن دانيال قدم تقريراً ، وساعد كيريل عقلياً.

بناء اللغة


لدينا معيار الأداء - direct . هذه دالة تزيد متغيرًا ولم تعد تقوم بأي شيء.

 //   var testInt64 int64 func BenchmarkDirect(b *testing.B) { for i := 0; i < bN; i++ { incDirect() } } func incDirect() { testInt64++ } 

نتيجة الوظيفة 1.46 نانو ثانية لكل عملية . هذا هو الخيار الأدنى. أسرع من 1.5 ns لكل عملية ، وربما لن تعمل.

تأجيل كيف نحبه


يعرف الكثيرون ويحبون استخدام لغة التأجيل. في كثير من الأحيان نستخدمها مثل هذا.

 func BenchmarkDefer(b *testing.B) { for i := 0; i < bN; i++ { incDefer() } } func incDefer() { defer incDirect() } 

لكن لا يمكنك استخدامها هكذا! كل تأجيل يأكل 40 نانوغرام لكل عملية.

 //   BenchmarkDirect-4 2000000000 1.46 / // defer BenchmarkDefer-4 30000000 40.70 / 

اعتقدت ربما هذا بسبب المضمنة؟ ربما مضمنة سريع جدا؟

Direct مضمّنة ، ولا يمكن أن تكون وظيفة التأجيل مضمنة. لذلك ، جمعت وظيفة اختبار منفصلة دون مضمنة.

 func BenchmarkDirectNoInline(b *testing.B) { for i := 0; i < bN; i++ { incDirectNoInline() } } //go:noinline func incDirectNoInline() { testInt64++ } 

لم يتغير شيء ، استغرق تأجيل نفس 40 نانوثانية. تأجيل عزيزي ، ولكن ليس كارثية.

عندما تستغرق الوظيفة أقل من 100 ns ، يمكنك الاستغناء عن التأجيل.

ولكن عندما تستغرق الوظيفة أكثر من جزء من الثانية ، فكل شيء على حاله - يمكنك استخدام التأجيل.

تمرير المعلمة حسب المرجع


النظر في خرافة شعبية.

 func BenchmarkDirectByPointer(b *testing.B) { for i := 0; i < bN; i++ { incDirectByPointer(&testInt64) } } func incDirectByPointer(n *int64) { *n++ } 

لم يتغير شيء - لا شيء يستحق كل هذا العناء.

 //     BenchmarkDirectByPointer-4 2000000000 1.47 / BenchmarkDeferByPointer-4 30000000 43.90 / 

باستثناء 3 ns لكل تأجيل ، ولكن يتم شطب هذا للتقلبات.

وظائف مجهولة


في بعض الأحيان يسأل newbies ، "هل وظيفة مجهولة المصدر باهظة الثمن؟"

 func BenchmarkDirectAnonymous(b *testing.B) { for i := 0; i < bN; i++ { func() { testInt64++ }() } } 

وظيفة مجهولة المصدر ليست مكلفة ، يستغرق 40.4 نانوثانية.

واجهات


هناك واجهة وهيكل ينفذ ذلك.

 type testTypeInterface interface { Inc() } type testTypeStruct struct { n int64 } func (s *testTypeStruct) Inc() { s.n++ } 

هناك ثلاثة خيارات لاستخدام طريقة الزيادة. مباشرة من الهيكل: var testStruct = testTypeStruct{} .

من الواجهة الخرسانية المقابلة: var testInterface testTypeInterface = &testStruct .

مع تحويل واجهة وقت التشغيل: var testInterfaceEmpty interface{} = &testStruct .

أدناه هو تحويل واجهة التشغيل والاستخدام مباشرة.

 func BenchmarkInterface(b *testing.B) { for i := 0; i < bN; i++ { testInterface.Inc() } } func BenchmarkInterfaceRuntime(b *testing.B) { for i := 0; i < bN; i++ { testInterfaceEmpty.(testTypeInterface).Inc() } } 

الواجهة ، على هذا النحو ، لا تكلف شيئًا.

 //  BenchmarkStruct-4 2000000000 1.44 / BenchmarkInterface-4 2000000000 1.88 / BenchmarkInterfaceRuntime-4 200000000 9.23 / 


تكاليف تحويل واجهة وقت التشغيل ، ولكنها ليست باهظة الثمن - لست بحاجة إلى الرفض على وجه التحديد. لكن حاول الاستغناء عنها حيثما أمكن ذلك.

الأساطير:

  • Dereference - مؤشرات إلغاء التسجيل - مجانًا.
  • ميزات مجهولة مجانية.
  • واجهات مجانية.
  • تحويل واجهة وقت التشغيل - ليست مجانية.

التبديل ، خريطة وشريحة


كل الوافد الجديد إلى Go يسأل عما يحدث إذا استبدلت التبديل بالخريطة. هل سيكون أسرع؟

التبديل يأتي في أحجام مختلفة. لقد اختبرت على ثلاثة أحجام: صغير لمدة 10 حالات ، متوسط ​​لـ 100 وكبير لـ 1000 حالة. تم العثور على رمز التبديل لـ 1000 حالة في رمز الإنتاج الحقيقي. بالطبع ، لا أحد يكتبهم بيديه. هذا رمز تم إنشاؤه تلقائيًا ، وعادةً ما يكون مفتاح كتابة. تم اختباره على نوعين: int و string. يبدو أنه سيتحول بشكل أكثر وضوحا.

التبديل قليلا. أسرع خيار هو التبديل الفعلي. يتبع ذلك على الفور يذهب شريحة ، حيث يحتوي مؤشر عدد صحيح المقابلة على إشارة إلى وظيفة. الخريطة ليست قائدًا على int أو السلسلة.
BenchmarkSwitchIntSmall-45000000003.26 ns / op
BenchmarkMapIntSmall-410000000011.70 ns / op
BenchmarkSliceIntSmall-45000000003.85 ns / op
BenchmarkSwitchStringSmall-410000000012.70 ns / op
BenchmarkMapStringSmall-410000000015.60 نانو ثانية / المرجع

التبديل على الأوتار أبطأ بكثير من التشغيل. إذا أمكنك إجراء التبديل لا إلى سلسلة ، ولكن إلى int ، فقم بذلك.

التبديل الأوسط. التبديل لا يزال يحكم int ، لكن الشريحة تجاوزته قليلاً. الخريطة لا تزال سيئة. ولكن على مفتاح سلسلة ، الخريطة أسرع من التبديل - كما هو متوقع.
BenchmarkSwitchIntMedium-43000000004.55 ns / op
BenchmarkMapIntMedium-410000000017.10 ns / op
BenchmarkSliceIntMedium-43000000003.76 ns / op
BenchmarkSwitchStringMedium-45000000028.50 ns / op
BenchmarkMapStringMedium-410000000020.30 ns / op

مفتاح كبير. ألف حالة تظهر الانتصار غير المشروط للخريطة في الترشيح "التبديل بسلسلة". من الناحية النظرية ، فازت شريحة ، ولكن في الممارسة العملية أنصحك باستخدام نفس التبديل هنا. لا تزال الخريطة بطيئة ، حتى لو أخذنا في الاعتبار أن الخريطة بها مفاتيح عدد صحيح مع وظيفة تجزئة خاصة. بشكل عام ، هذه الوظيفة لا تفعل شيئًا. و int نفسها لديها التجزئة ل ​​int.
BenchmarkSwitchIntLarge-410000000013.6 ns / op
BenchmarkMapIntLarge-45000000034.3 ns / op
BenchmarkSliceIntLarge-410000000012.8 ns / op
BenchmarkSwitchStringLarge-420000000100.0 ns / op
BenchmarkMapStringLarge-43000000037.4 ns / op

الاستنتاجات. الخريطة أفضل فقط على الكميات الكبيرة وليس في حالة عدد صحيح. أنا متأكد من أنه في أي من الحالات باستثناء int ، سيتصرف كما هو الحال في السلسلة. تقسم الشريحة دائمًا عندما تكون الظروف عددًا صحيحًا. استخدمه إذا كنت ترغب في "تسريع" البرنامج بمقدار 2 نانو ثانية.

التفاعل بين الروتين


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

  • الذرية. هذه وسيلة للتطبيق المحدود - يمكنك استبدال المؤشر أو استخدام int.
  • تم استخدام Mutex على نطاق واسع منذ Java.
  • القناة فريدة من نوعها لـ GO.
  • قناة مخزنة - قنوات مخزنة.

بالطبع ، لقد اختبرت على عدد أكبر بكثير من goroutines التي تتنافس على مورد واحد. لكنه اختار ثلاثة لنفسه كدليل: القليل - 100 ، المتوسط ​​- 1000 والكثير - 10000.

ملف تعريف التحميل مختلف . في بعض الأحيان يريد كل gorutins الكتابة في متغير واحد ، ولكن هذا أمر نادر الحدوث. عادة ، بعد كل شيء ، يكتب البعض ، البعض يقرأ. من أكثر القراء قراءة - 90٪ ، من يكتبون - 90٪ يكتبون.

هذا هو الكود المستخدم حتى يتمكن goroutine الذي يخدم القناة من توفير كل من القراءة والكتابة إلى متغير.

 go func() { for { select { case n, ok := <-cw: if !ok { wgc.Done() return } testInt64 += n case cr <- testInt64: } } }() 

إذا وصلت إلينا رسالة عبر القناة التي نكتب من خلالها ، فإننا ننفذها. إذا تم إغلاق القناة ، فإننا ننهي goroutin. في أي وقت ، نحن على استعداد للكتابة إلى القناة التي يستخدمها goroutines الآخرين للقراءة.
BenchmarkMutex-410000000016.30 نانوغرام / المرجع
BenchmarkAtomic-42000000006.72 ns / op
BenchmarkChan-45000000239.00 ns / op

هذه هي البيانات ل goroutine واحد. يتم إجراء اختبار القناة على اثنين من goroutines: أحدهما يعالج القناة ، والآخر يكتب على هذه القناة. وقد تم اختبار هذه الخيارات على واحد.

  • يكتب مباشرة إلى متغير.
  • Mutex يأخذ سجل ، يكتب إلى متغير ويصدر سجل.
  • يكتب الذرية إلى متغير من خلال الذرية. أنها ليست مجانية ، لكنها لا تزال أرخص بكثير من Mutex على جاروتن واحد.

مع كمية صغيرة من goroutine ، فإن Atomic هو وسيلة فعالة وسريعة للتزامن ، وهو أمر غير مفاجئ. Direct غير موجود هنا ، لأننا نحتاج إلى التزامن ، والذي لا يوفره. لكن الذرة لديها عيوب ، بالطبع.
BenchmarkMutexFew-43000055894 ns / op
BenchmarkAtomicFew-410000014585 ns / op
BenchmarkChanFew-45000323859 ns / op
BenchmarkChanBufferedFew-45000341321 ns / op
BenchmarkChanBufferedFullFew-42000070052 ns / op
BenchmarkMutexMostlyReadFew-43000056402 ns / op
BenchmarkAtomicMostlyReadFew-410000002094 ns / op
BenchmarkChanMostlyReadFew-43000442689 ns / op
BenchmarkChanBufferedMostlyReadFew-43000449666 ns / op
BenchmarkChanBufferedFullMostlyReadFew-45000442708 ns / op
BenchmarkMutexMostlyWriteFew-42000079708 ns / op
BenchmarkAtomicMostlyWriteFew-410000013358 ns / op
BenchmarkChanMostlyWriteFew-43000449556 ns / op
BenchmarkChanBufferedMostlyWriteFew-43000445423 ns / op
BenchmarkChanBufferedFullMostlyWriteFew-43000414626 ns / op

يصل المقبل هو Mutex. كنت أتوقع أن تكون القناة في أسرع وقت مثل Mutex ، لكن لا.

القناة هي أمر من حيث الحجم أغلى من Mutex.

علاوة على ذلك ، يتم عرض القناة والقناة المخزنة في نفس السعر تقريبًا. وهناك قناة ، حيث لا يفيض المخزن المؤقت مطلقًا. إنه ترتيب من حيث الحجم أرخص من الذي يفيض المخزن المؤقت. فقط إذا لم يكن المخزن المؤقت في القناة ممتلئًا ، فإنه يكلف نفسه تقريبًا في أوامر الحجم مثل Mutex. هذا ما كنت أتوقعه من الاختبار.

يتم تكرار هذه الصورة مع توزيع مقدار تكلفتها على أي ملف تعريف تحميل - على MostlyRead وعلى MostlyWrite. علاوة على ذلك ، تكلف قناة MostlyRead الكاملة نفس التكلفة غير المكتملة. وقناة MostlyWrite المخزنة مؤقتًا ، والتي يكون المخزن المؤقت فيها غير ممتلئ ، تكلف نفس تكلفة الباقي. لا أستطيع أن أقول سبب ذلك - لم أدرس هذه المشكلة بعد.

تمرير المعلمات


كيفية تمرير المعلمات بشكل أسرع - بالرجوع أو بالقيمة؟ دعونا التحقق من ذلك.

راجعت على النحو التالي - أنواع المتداخلة الصنع 1-10.

 type TP001 struct { I001 int64 } type TV002 struct { I001 int64 S001 TV001 I002 int64 S002 TV001 } 

سيكون للنوع المتداخل العاشر 10 حقول int64 ، والأنواع المتداخلة للتداخل السابق ستكون أيضًا 10.

ثم كتب الوظائف التي تخلق نوعًا من التعشيش.

 func NewTP001() *TP001 { return &TP001{ I001: rand.Int63(), } } func NewTV002() TV002 { return TV002{ I001: rand.Int63(), S001: NewTV001(), I002: rand.Int63(), S002: NewTV001(), } } 

للاختبار ، استخدمت ثلاثة خيارات من النوع: صغير مع تداخل 2 ، متوسط ​​مع تداخل 3 ، كبير مع تداخل 5. اضطررت إلى إجراء اختبار كبير جدًا مع تداخل 10 في الليل ، ولكن هناك الصورة نفسها تمامًا مثل 5.

في الوظائف ، يكون تمرير القيمة ضعف سرعة المرور بالرجوع . هذا يرجع إلى حقيقة أن تمرير القيمة لا يتم تحميل تحليل الهروب. وفقا لذلك ، فإن المتغيرات التي نخصصها هي على المكدس. أنها أرخص بكثير لوقت التشغيل ، لجامع القمامة. على الرغم من أنه قد لا يكون لديه الوقت للاتصال. استمرت هذه الاختبارات لبضع ثوان - ربما كان جامع القمامة لا يزال نائماً.
BenchmarkCreateSmallByValue-42000008942 ns / op
BenchmarkCreateSmallByPointer-410000015985 ns / op
BenchmarkCreateMediuMByValue-42000862317 ns / op
BenchmarkCreateMediuMByPointer-420001228130 ns / op
BenchmarkCreateLargeByValue-43047398456 ns / op
BenchmarkCreateLargeByPointer-42061928751 ns / op

السحر الاسود


هل تعرف ماذا سينتج هذا البرنامج؟

 package main type A struct { a, b int32 } func main() { a := new(A) aa = 0 ab = 1 z := (*(*int64)(unsafe.Pointer(a))) fmt.Println(z) } 

تعتمد نتيجة البرنامج على الهيكل الذي تم تنفيذه عليه. على endian قليلاً ، على سبيل المثال ، AMD64 ، يعرض البرنامج 232. على endian كبيرة ، واحدة. والنتيجة مختلفة ، لأن هذه الوحدة تظهر في منتصف الرقم ، وفي النهاية endian الكبيرة ، في النهاية.

لا تزال هناك معالجات في العالم حيث مفاتيح endian ، على سبيل المثال ، Power PC. سيكون من الضروري معرفة نوع endian الذي تم تكوينه على جهاز الكمبيوتر الخاص بك عند بدء التشغيل ، قبل إجراء استنتاجات حول ما تفعله الحيل غير الآمنة. على سبيل المثال ، إذا قمت بكتابة رمز Go الذي سيتم تنفيذه على بعض خادم IBM متعدد المعالجات.

لقد استشهدت بهذا الكود لشرح لماذا أعتبر كل السحر الأسود غير الآمن. لا تحتاج إلى استخدامه. لكن سيريل يعتقد أنه ضروري. وهنا السبب.

هناك وظيفة تفعل نفس الشيء مثل GOB - Go Binary Marshaller. هذا هو التشفير ، ولكن على غير آمنة.

 func encodeMut(data []uint64) (res []byte) { sz := len(data) * 8 dh := (*header)(unsafe.Pointer(&data)) rh := &header{ data: dh.data, len: sz, cap: sz, } res = *(*[]byte)(unsafe.Pointer(&rh)) return } 

في الواقع ، فإنه يأخذ قطعة من الذاكرة ويسحب مجموعة من بايت منه.

هذا ليس حتى أمر - هذه هي أمرين. لذلك ، عندما لا يكتب Cyril Danshin مدونة عالية الأداء ، لا يتردد في الدخول في أحشاء برنامجه وجعله غير آمن.

المعيار gob-42000008466 ns / op120.94 ميغابايت / ثانية
BenchmarkUnsafeMut-45000000037 ن / ثانية27691.06 ميغابايت / ثانية
سنناقش ميزات أكثر تحديدًا من Go في 7 أكتوبر في GolangConf - مؤتمر لأولئك الذين يستخدمون Go في التطوير المهني وأولئك الذين يعتبرون هذه اللغة بديلاً. Daniil Podolsky هو مجرد عضو في لجنة البرنامج ، إذا كنت ترغب في مناقشة هذه المقالة أو الكشف عن المشكلات ذات الصلة - قم بتقديم طلب للحصول على تقرير.

لكل شيء آخر ، فيما يتعلق بالأداء العالي ، بالطبع ، HighLoad ++ . نحن نقبل أيضا الطلبات هناك. الاشتراك في النشرة الإخبارية والبقاء على اطلاع على الأخبار من جميع مؤتمراتنا لمطوري الويب.

Source: https://habr.com/ru/post/ar461291/


All Articles