مدونة

عمليات معالجة الشرائح المدمجة في Go غير مكتملة

غياب طريقة عالية الأداء لدمج 3 شرائح أو أكثر

في Go، هناك ثلاث دوال مدمجة لمعالجة الشرائح:

  1. تُستخدم دالة make لإنشاء شريحة بطول وسعة محددين، وتُضبط جميع العناصر على القيم الصفرية. 1. تُستخدم دالة copy لنسخ بعض العناصر من شريحة إلى أخرى إذا كان للشريحتين نوع العنصر نفسه. 1. تُستخدم دالة append لإلحاق بعض العناصر بشريحة أساسية وينتج عن ذلك شريحة جديدة. وقد تتشارك الشريحة الأساسية جميع عناصرها مع الشريحة الجديدة أو لا، حسب ما إذا كانت سعة الشريحة الأساسية كبيرة بما يكفي لاستيعاب جميع العناصر الملحقة.

عندما لا تكون سعة الشريحة الأساسية في استدعاء دالة append كبيرة بما يكفي، يمكن اعتبار الاستدعاء طريقة لدمج شريحتين. ولأن الطريقة مدمجة، فهي عالية الأداء.

ومع ذلك، لا توجد طريقة عالية الأداء لدمج 3 شرائح أو أكثر. وقد يكون التنفيذ التالي الطريقة الأكثر أداءً لإنجاز المهمة:


func merge[S ~[]E, E any](ss ...S) S {
	var n, allNils, k = 0, true, -1
	for i := range ss {
		if m := len(ss[i]); n != 0 {
			n += m
			if n < 0 {
				panic("sum of lengths is too large")
			}
		} else if m > 0 {
			n = m
			allNils = false
			k = i
		} else {
			allNils = allNils && ss[i] == nil
		}
	}
	if allNils {
		return nil
	}
	if n == 0 {
		return S{}
	}

	// استفد من هذا التحسين:
	// https://github.com/golang/go/commit/6ed4661807b219781d1aa452b7f210e21ad1974b
	s := ss[k]
	r := make(S, n)
	copy(r, s)
	r = r[:len(s)]
	ss = ss[k+1:]

	for _, s := range ss {
		r = append(r, s...)
	}
	return r
}

عموماً، سيُصفِّر استدعاء make العناصر المخصَّصة أثناء تنفيذه، وهو أمر غير ضروري في حالة الاستخدام هذه تحديداً. ويبذل التنفيذ قصارى جهده لتصفير أقل عدد ممكن من العناصر داخل استدعاء make؛ فباستخدام التحسين المذكور لا يُصفِّر العناصر في r[:len(ss[0])]، لكنه لا يزال يفشل في ذلك بالنسبة للعناصر المتبقية (تلك الموجودة في r[len(ss[0]):]).

لو كانت دالة merge مدمجة، لأمكنها تجنب جميع عمليات تصفير العناصر غير الضرورية.

غياب طريقة نظيفة لتقليم الشرائح

كما ذُكر أعلاه، سيعيد استدعاء دالة append استخدام المصفوفة الكامنة (backing array) الخاصة بمعطى الشريحة الأول (نسميه هنا base) من أجل الشريحة الناتجة إذا كانت سعة base كبيرة بما يكفي لاستيعاب جميع العناصر الملحقة. وفي بعض الحالات، نريد منع استدعاء append من تعديل العناصر في base[len(base):]، ونحقق ذلك بتقليم سعة base لتساوي طوله:


var x, y, z T

	... = append(base[:len(base):len(base)], x, y, z)

لا شيء يستحق الشكوى منه، إلا أنه مطوّل بعض الشيء، خصوصاً عندما يكون تعبير base مطوّلاً، مثل


	... = append(aValue.Field.Slice[:len(aValue.Field.Slice):len(aValue.Field.Slice)], x, y, z)

إذا كان تعبير base استدعاء دالة، فيجب تخزين نتيجة الاستدعاء في متغير وسيط مؤقت:


	base := aFunctionCall()
	... = append(base[:len(base):len(base)], x, y, z)

يمكننا استخدام دالة Clip في حزمة golang.org/x/exp/slices، لكن هذه الطريقة ليست نظيفة بما يكفي بعد.


import "golang.org/x/exp/slices"

	... = append(slices.Clip((base), x, y, z)

	... = append(slices.Clip(aValue.Field.Slice), x, y, z)

	... = append(slices.Clip(aFunctionCall()), x, y, z)

أفضّل شخصياً هذا المقترح، لكنه رُفض:


aSlice[ : n : ]  // <=> aSlice[ : n : n]
aSlice[m : n : ] // <=> aSlice[m : n : n]
sSlice[ : : ]    // <=> aSlice[ : len(aSlice) : len(aSlice)]
aSlice[m : : ]   // <=> aSlice[m : len(aSlice) : len(aSlice)]

لو اعتُمد المقترح، لأمكننا ببساطة كتابة الشيفرة هكذا


	... = append(base[::], x, y, z)

	... = append(aValue.Field.Slice[::], x, y, z)

	... = append(aFunctionCall()[::], x, y, z)

وهو أنظف بكثير.

غياب طريقة آمنة للكشف عن تداخل العناصر بين شريحتين

تمنع هذه الحقيقة حزم Go المخصصة من تقديم تنفيذات مثالية لبعض عمليات معالجة الشرائح.

بعض الحزم القياسية تستخدم (وقد استخدمت سابقاً) طرقاً غير آمنة للقيام بالمهمة. على سبيل المثال:

هذه الاستخدامات غير الآمنة لا تتبع فعلياً أي أنماط استخدام صالحة لـ unsafe، لذا فهي تنفيذات سيئة وخطيرة.