هل الكود الموضح أدناه مكافئ في الأداء؟
// (A). HasPrefix . return strings.HasPrefix(s, "#") // (B). HasPrefix. return len(s) >= len("#") && s[:len("#")] == "#"
الجواب هو لا .
للحصول على تفاصيل وتفسيرات ، أطلب تحت القط.
يوم جيد ، قبل أن تفتح الموضوع ، أود أن أقدم نفسي.
اسمي Iskander ، ومن وقت لآخر أرسل إرسالًا إلى مستودع golang / go .

اعتدت القيام بذلك نيابة عن فريق Intel Go ، لكن مساراتنا تباينت والآن أنا مساهم مستقل. لقد عملت مؤخرًا في vk في فريق البنية التحتية.
في وقت فراغي ، أصنع أدوات مختلفة لـ Go ، مثل go-الناقد والتوافق . أود أيضا أن ألفت الغرام .
قياسه!
تابع على الفور المقارنة وتحديد الخطوط العريضة:
package benchmark import ( "strings" "testing" ) var s = "#string" func BenchmarkHasPrefixCall(b *testing.B) { for i := 0; i < bN; i++ { _ = strings.HasPrefix(s, "#") _ = strings.HasPrefix(s, "x") } } func BenchmarkHasPrefixInlined(b *testing.B) { for i := 0; i < bN; i++ { _ = len(s) >= len("#") && s[:len("#")] == "#" _ = len(s) >= len("x") && s[:len("x")] == "x" } }
بدلا من التوصية benchstat لك ، وسوف تظهر لك benchrun .
باستخدام أمر واحد ، يمكننا تشغيل كلا المعيارين والحصول على مقارنة:
go-benchrun HasPrefixCall HasPrefixInlined -v -count=10 . Benchstat results: name old time/op new time/op delta HasPrefixCall-8 9.15ns ± 1% 0.36ns ± 3% -96.09% (p=0.000 n=10+9)
يكون الخيار الذي يتضمن التضمين اليدوي أسرع بكثير من الكود الذي تم الحصول عليه عن طريق دمج نص الدالة مع المترجم. دعنا نحاول معرفة سبب حدوث ذلك.
سلاسل
أذكر تنفيذ strings.HasPrefix
// HasPrefix tests whether the string s begins with prefix. func HasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[0:len(prefix)] == prefix }
تم HasPrefix
الدالة HasPrefix
بواسطة برنامج التحويل البرمجي.
يمكنك التحقق من ذلك على النحو التالي:
go build -gcflags='-m=2' strings 2>&1 | grep 'can inline HasPrefix'
للاتصال strings.HasPrefix
من الخيار (A)
نحصل على رمز الجهاز التالي:
MOVQ (TLS), CX CMPQ SP, 16(CX) JLS more_stack fn_body: SUBQ $40, SP MOVQ BP, 32(SP) LEAQ 32(SP), BP XCHGL AX, AX MOVQ s+56(SP), AX CMPQ AX, $1 JGE compare_strings XORL AX, AX MOVB AL, ~ret1+64(SP) MOVQ 32(SP), BP ADDQ $40, SP return: RET compare_strings: MOVQ s+48(SP), AX MOVQ AX, (SP) LEAQ go.string."#"(SB), AX MOVQ AX, 8(SP) MOVQ $1, 16(SP) CALL runtime.memequal(SB) MOVBLZX 24(SP), AX JMP return more_stack: CALL runtime.morestack_noctxt(SB) JMP fn_body
تجاهل حقيقة أن الرمز يشبه الشعرية.
ما يجب الانتباه إليه:
strings.HasPrefix
حقا تم إدراجها ، أي مكالمة.- لمقارنة السلاسل ، يتم
runtime.memequal
.
ولكن ما الذي يتم إنشاؤه بعد ذلك للإصدار المدمج يدويًا ، الرمز من المثال (B)
؟
MOVQ s+16(SP), AX CMPQ AX, $1 JLT different_length MOVQ s+8(SP), AX CMPB (AX), $35 // 35 - "#" SETEQ AL return: MOVB AL, "".~ret1+24(SP) RET different_length: XORL AX, AX JMP 22
وهنا المحول البرمجي لا يُنشئ اتصالًا بـ runtime.memequal
ويتم مقارنة حرف واحد مباشرةً. من الناحية المثالية ، كان ينبغي عليه فعل الشيء نفسه بالنسبة للخيار الأول.
نلاحظ الجانب الضعيف من مُحسِّن Go ، وسوف نقوم بتحليله.
تعبير ثابت التعبير
السبب في أنه يمكن تحسين استدعاء strings.HasPrefix(s, "#")
هو أن وسيطة البادئة ثابتة. نحن نعرف طوله ومحتوياته. ليس من المنطقي استدعاء runtime.memequal
للسلاسل القصيرة ، فمن الأسرع إجراء مقارنة بين الأحرف "في مكان" ، وتجنب مكالمة إضافية.
كما تعلم ، فإن المترجمين عادة ما يكون لديهم جزأين على الأقل: الواجهة الأمامية للمترجم وخلفية المترجم . الأول يعمل مع عرض مستوى أعلى ، والثاني هو أقرب إلى الجهاز وسيبدو العرض الوسيط مثل دفق من التعليمات. استخدمت عدة إصدارات من Go بالفعل تمثيل SSA للتحسينات في الجزء الخلفي من برنامج التحويل البرمجي.
يتم طي ثابت ، مثل {10*2 => 20}
، في الخلفية. بشكل عام ، تقع معظم العمليات المرتبطة بخفض التكلفة الحسابية للتعبيرات في هذا الجزء من المجمع. ولكن هناك استثناءات.
استثناء واحد هو تحسين مقارنات السلسلة الثابتة. عندما يرى المحول البرمجي مقارنة سلسلة (أو سلسلة فرعية) يكون فيها أحد runtime.memequal
أو كلاهما ثوابت ، يتم إنشاء رمز أكثر كفاءة من استدعاء runtime.memequal
.
يمكنك رؤية الكود المصدري المسؤول عن ذلك في ملف cmd / compile / internal / gc / walk.go: 3362 .
يحدث دمج الوظائف قبل إطلاق هذه التحسينات ، ولكن أيضًا في الجزء الأمامي من برنامج التحويل البرمجي.
يبدو أن كل نفس لا يسمح هذا التحسين للعمل في حالتنا؟
كيف يذهب تضمين وظيفة المكالمات
إليك كيفية حدوث التضمين:
// : return strings.HasPrefix(s, "#") // : func HasPrefix(s, prefix string) bool // : _s, _prefix := s, "#" return len(s) >= len(prefix) && s[:len(prefix)] == prefix
عند تضمين الدوال ، يقوم المحول البرمجي بتعيين الوسائط للمتغيرات المؤقتة ، والتي تقطع التحسينات ، حيث إن الخوارزمية في walk.go لا ترى ثوابت ، ولكن الوسائط مع المتغيرات. هذه هي المشكلة.
بالمناسبة ، هذا لا يتعارض مع تحسينات الواجهة الخلفية التي تحت تصرف SSA. ولكن هناك مشاكل أخرى ، على سبيل المثال ، عدم القدرة على استعادة التراكيب اللغوية عالية المستوى لمقارنتها بفعالية (تم "التخطيط" للعمل على إزالة هذا العيب لعدة سنوات).
مثال آخر: تحليل الهروب
تخيل وظيفة مهمة لتخصيص مخزن مؤقت مؤقت على المكدس:
func businessLogic() error { buf := make([]byte, 0, 16) // buf // . return nil }
نظرًا لأن buf
لا "يهرب" ، سيتمكن المحول البرمجي من تخصيص هذه 16 بايت على المكدس ، دون تخصيص على الكومة. مرة أخرى ، كل ذلك بفضل القيمة الثابتة عند make
الاتصال. لتخصيص ذاكرة على المكدس ، من المهم بالنسبة لنا معرفة الحجم المطلوب ، والذي سيكون جزءًا من الإطار المخصص لاستدعاء الوظيفة.
لنفترض في المستقبل أننا أردنا تخصيص مخازن مؤقتة ذات أحجام مختلفة وتغليف بعض المنطق في الأساليب. قدمنا تجريدًا جديدًا وقررنا استخدام نوع tmpBuf
الجديد في tmpBuf
. وظيفة التصميم بسيطة للغاية:
func newTmpBuf(sizeHint int) tmpBuf { return tmpBuf{buf: make([]byte, 0, sizeHint)} }
تكييف المثال الأصلي:
func businessLogic() error { - buf := make([]byte, 0, 16) + buf := newTmpBuf(16) // buf // . return nil }
سيتم تضمين المُنشئ ، لكن التخصيص سيكون الآن دائمًا على كومة الذاكرة المؤقتة ، لنفس السبب الذي يتم من خلاله تمرير المتغيرات عبر متغيرات مؤقتة. سيرى تحليل الهروب make([]byte, 0, _sizeHint)
لا يندرج تحت أنماط التعرف الخاصة به make
مكالمات محسّنة.
إذا كان لدينا "كل شيء يشبه البشر" ، فلن توجد المشكلة ، بعد تضمين مُنشئ newTmpBuf
فسيكون من الواضح أن الحجم ما زال معروفًا في مرحلة التجميع.
هذا يزعج أكثر من الموقف مع مقارنة السلاسل.
آفاق الذهاب 1.13
يمكن تصحيح الموقف بسهولة وقد أرسلت بالفعل الجزء الأول من القرار .

إذا كنت تعتقد أن المشكلة الموضحة في المقالة تحتاج حقًا إلى حل ، فالرجاء وضع إبهام في المشكلة المقابلة .
موقفي هو أن تضمين التعليمات البرمجية بيديك لمجرد أنها تعمل بشكل أسرع في الإصدار الحالي من Go غير صحيح. من الضروري إصلاح هذا العيب في المُحسِّن ، على الأقل إلى الحد الذي تعمل فيه الأمثلة الموضحة أعلاه دون انحدارات الأداء غير المتوقعة.
إذا سارت الأمور وفقًا للخطة ، فسيتم تضمين هذا التحسين في إصدار Go 1.13.
شكرا لاهتمامكم
إضافة: الحل المقترح
هذا القسم هو الأشجع ، أولئك الذين لا تعبوا من القراءة.
لذلك ، لدينا العديد من الأماكن التي تعمل بشكل أسوأ عند استخدام المتغيرات بدلاً من قيمها مباشرةً. يتمثل الحل المقترح في تقديم وظيفة جديدة في الجزء الأمامي من الجزء المترجم ، والتي تتيح لك الحصول على آخر قيمة مرتبطة بالاسم. بعد ذلك ، في كل عملية تحسين تتوقع قيمة ثابتة ، لا تستسلم عند اكتشاف متغير ، ولكن تتلقى هذه الحالة المحفوظة مسبقًا.
قد يبدو توقيع الميزة الجديدة لدينا كما يلي:
func getConstValue(n *Node) *Node
يمكن العثور على تعريف Node
في ملف syntax.go .
يحتوي كل تعريف متغير على علامة Node
مع علامة ONAME
. داخل Node.Name.Defn
معظم هذه المتغيرات لها قيمة تهيئة.
إذا كانت Node
حرفيًا بالفعل ، فلست بحاجة إلى القيام بأي شيء وسنقوم بإرجاع n
. إذا كان هذا ONAME
(متغير) ، فيمكنك محاولة استخراج نفس قيمة التهيئة من n.Name.Defn
.
ولكن ماذا عن التعديلات بين التصريح وقراءة المتغير الذي نسميه getConstValue
؟ إذا قمنا بتقييد أنفسنا على متغيرات للقراءة فقط ، فلا توجد مشكلة. تحتوي الواجهة الأمامية لـ Go على علامات عقدة خاصة تحدد أسماء مشابهة. إذا تم تعديل المتغير ، فلن يقوم getConstValue
بإرجاع قيمة تهيئة.
لا يقوم المبرمجون ، كقاعدة عامة ، بتعديل وسيطات الإدخال لأنواع رقمية وسلسلة ، وهذا يجعل من الممكن تغطية عدد كبير من الحالات باستخدام هذه الخوارزمية البدائية.
نحن الآن على استعداد للنظر في التنفيذ:
func getConstValue(n *Node) *Node { // ONAME definition. if n.Op != ONAME || n.Name.Defn == nil { return n } // , . // , , // escape analysis' . maybeModified := n.Assigned() || n.Name.Defn.Assigned() || n.Addrtaken() if maybeModified { return n } // OAS - Node . // n.Name.Defn.Left - LHS. // n.Name.Defn.Right - RHS. // consttype(v) . // CTxxx, . if n.Name.Defn.Op == OAS { v := n.Name.Defn.Right if v != nil && consttype(v) != CTxxx { return v } } return n }
إليك كيفية تغيير الرمز ، والذي يعتمد على الثوابت:
- i := indexconst(r) + i := indexconst(getConstValue(r))
عظيم ، وحتى يعمل:
n := 10 xs := make([]int, n) // !
قبل هذا التغيير ، لم يتمكن تحليل الهروب من الحصول على القيمة من 10
إلى n
، ولهذا السبب افترضت الحاجة إلى وضع xs
على الكومة.
يشبه الكود أعلاه بناءً على الموقف الذي لوحظ أثناء التضمين. قد يكون n
متغير مؤقت يتم إضافته عند تمرير الوسيطة.
لسوء الحظ ، هناك فروق دقيقة.
لقد قمنا بحل مشكلة المتغيرات المحلية المقدمة من خلال OAS ، لكن Go يقوم بتهيئة المتغيرات الخاصة بالوظائف المدمجة من خلال OAS2 . لهذا السبب ، نحتاج إلى تغيير getConstValue
يعمل على getConstValue
وظيفة getConstValue
ويعدل قليلاً رمز الكود الداخلي نفسه ، لأنه من بين أشياء أخرى ، لا يحتوي Defn
حقل Defn
مناسب.
كانت تلك أنباء سيئة. أخبار سارة : ظهرت #gocontributing channel في الركود باللغة الروسية ، حيث يمكنك مشاركة أفكارك وخططك وطرح الأسئلة ومناقشة كل ما يتعلق بالمشاركة في Go development.