في هذه الحالة ، ستكون الزيادة في عناصر
std :: vector أسرع - إذا كانت من النوع
uint8_t أو
uint32_t ؟
من أجل عدم التفكير بصورة مجردة ، فإننا نعتبر تطبيقين محددين:
void vector8_inc(std::vector<uint8_t>& v) { for (size_t i = 0; i < v.size(); i++) { v[i]++; } } void vector32_inc(std::vector<uint32_t>& v) { for (size_t i = 0; i < v.size(); i++) { v[i]++; } }
دعنا نحاول تخمين
من السهل الإجابة على هذا السؤال باستخدام المعيار ، وبعد ذلك بقليل سنقوم بذلك ، لكن أولاً سنحاول تخمينه (يسمى هذا "التفكير بناءً على المبادئ الأساسية" - يبدو أشبه رائحة).
أولاً ، يجدر طرح سؤال:
ما هو حجم هذه المتجهات ؟
حسنا ، دعنا نختار بعض الأرقام. فليكن هناك 20000 عنصر في كل منها.
علاوة على ذلك ، من المعروف أننا سنختبر على معالج Intel Skylake - سنرى خصائص أوامر الإضافة لمعاملات
8 بت و
32 بت مع عنونة مباشرة. اتضح أن مؤشراتها الرئيسية هي نفسها: عملية واحدة لكل دورة وتأخير 4 دورات لكل وصول إلى الذاكرة (1). في هذه الحالة ، لا يهم التأخير ، حيث يتم تنفيذ كل عملية إضافة بشكل مستقل ، بحيث تكون السرعة المحسوبة عنصرًا واحدًا في كل دورة ، بشرط أن يتم تنفيذ بقية العمل على الحلقة بشكل متوازٍ.
يمكنك أيضًا ملاحظة أن 20000 عنصر يتوافق مع مجموعة بيانات 20 كيلوبايت للإصدار مع
uint8_t وما يصل إلى 80 كيلوبايت للإصدار مع
uint32_t . في الحالة الأولى ، تناسبها بشكل مثالي في ذاكرة التخزين المؤقت لمستوى L1 لأجهزة الكمبيوتر الحديثة التي تستند إلى x86 ، وفي الحالة الثانية - لا. اتضح أن إصدار 8 بت سوف تبدأ بداية بسبب التخزين المؤقت كفاءة؟
أخيرًا ، نلاحظ أن مهمتنا تشبه إلى حد كبير الحالة الكلاسيكية
للاتجاه التلقائي : في حلقة بها عدد معروف من التكرارات ، يتم إجراء عملية حسابية على عناصر موجودة بالتتابع في الذاكرة. في هذه الحالة ، يجب أن يكون للإصدار 8 بت ميزة هائلة على الإصدار 32 بت ، حيث أن عملية متجه واحد ستعالج أربعة أضعاف عدد العناصر ، وبصفة عامة تقوم معالجات Intel بعمليات متجهة على عناصر أحادية البايت بنفس السرعة كما في 32- عناصر قليلا.
حسنا ، توقف عن الصراخ. حان الوقت للانتقال إلى الاختبار.
المؤشر
حصلت على التوقيتات التالية لمتجهات مكونة من 20.000 عنصر في
مجمعي gcc 8 و
clang 8 بمستويات تحسين مختلفة:
اتضح أنه ، باستثناء مستوى
-O1 ، يكون الإصدار مع
uint32_t أسرع من الإصدار مع
uint8_t ، وفي بعض الحالات يكون مهمًا: 5.4 مرة على gcc على المستوى
-O3 و 8 مرات بالضبط على
clang على كلا المستويين ،
-O2 و
- O3 . نعم ، زيادة أعداد صحيحة 32 بت في
std :: vector تصل إلى ثمانية أضعاف سرعة زيادة عدد صحيح 8 بت على المحول البرمجي الشائع مع إعدادات التحسين القياسية.
كالعادة ، دعنا ننتقل إلى قائمة المجمعين على أمل أن تلقي الضوء على ما يحدث.
فيما يلي قائمة بـ gcc 8 على مستوى
-O2 ، حيث يكون الإصدار 8 بت "فقط" 1.5 مرة أبطأ من الإصدار 32 بت (2):
8 بت: .L3: inc BYTE PTR [rdx+rax] mov rdx, QWORD PTR [rdi] inc rax mov rcx, QWORD PTR [rdi+8] sub rcx, rdx cmp rax, rcx jb .L3
32-بت: .L9: inc DWORD PTR [rax] add rax, 4 cmp rax, rdx jne .L9
يبدو الإصدار 32 بت كما توقعنا تمامًا من حلقة غير مطورة (3): زيادة (4) مع عنوان ، ثم ثلاثة أوامر للتحكم في الحلقة:
add rax ،
4 - زيادة للمتغير الاستقرائي (5) واثنين من أوامر
cmp و
jne للتحقق من شروط الخروج من الحلقة والقفز الشرطي عليها. يبدو كل شيء رائعًا - قد يعوض النشر تكلفة زيادة العداد وفحص الحالة ، وسيصل رمزنا تقريبًا إلى الحد الأقصى للسرعة الممكنة لعنصر واحد لكل دورة ساعة (6) ، ولكن بالنسبة للتطبيق مفتوح المصدر ، فإنه سيفعل ذلك. وماذا عن الإصدار 8 بت؟ بالإضافة إلى الأمر
inc الذي يحتوي على العنوان ، يتم تنفيذ أمرين إضافيين للقراءة من الذاكرة ، بالإضافة إلى الأمر
الفرعي ، الذي يتم أخذه من أي مكان.
وهنا قائمة مع التعليقات:
8 بت: .L3: inc BYTE PTR [rdx+rax] ; v[i] mov rdx, QWORD PTR [rdi] ; v.begin inc rax ; i++ mov rcx, QWORD PTR [rdi+8] ; v.end sub rcx, rdx ; end - start (.. vector.size()) cmp rax, rcx ; i < size() jb .L3 ; . i < size()
هنا
vector :: start و
vector :: end هما المؤشرات الداخلية لـ
std :: vector ، والتي تستخدم للإشارة إلى بداية ونهاية تسلسل العناصر الموجودة داخل المنطقة المحددة لها (7) ، وهذه هي في الأساس نفس القيم والتي تستخدم لتنفيذ
vector :: تبدأ () و
vector :: end () (على الرغم من أنها من نوع مختلف). اتضح أن جميع الأوامر الإضافية هي مجرد نتيجة لحساب
vector.size () . لا يبدو شيئا غير عادي؟ ولكن في النهاية ، في الإصدار 32 بت ، بالطبع ،
يتم حساب
الحجم () أيضًا ، ومع ذلك ، لم تكن هذه الأوامر موجودة في هذه القائمة. حدث حساب
الحجم () مرة واحدة فقط - خارج الحلقة.
إذن ما الأمر؟ الجواب القصير هو
مؤشر التعرجات . سأقدم إجابة مفصلة أدناه.
إجابة مفصلة
يتم تمرير المتجه
v إلى الوظيفة حسب المرجع ، وهو في الواقع مؤشر ملثمين. يجب أن يذهب المحول البرمجي إلى الأعضاء
v :: start و
v :: end للمتجه لحساب
حجمه () ، وفي مثالنا ،
يتم حساب
الحجم () عند كل تكرار. لكن المترجم غير ملزم بأن يطيع الكود المصدري بشكل أعمى: قد يحمل نتيجة استدعاء الدالة
size () خارج الحلقة ، لكن فقط إذا علم أنه من المؤكد أن دلالات البرنامج
لن تتغير . من وجهة النظر هذه ، فإن المشكلة الوحيدة في الحلقة هي الزيادة
v [i] ++ . يتم التسجيل في عنوان غير معروف. هل يمكن لمثل هذه العملية أن تغير قيمة الحجم ()؟
إذا حدث السجل في
std :: vector <uint32_t> (بمعنى
uint32_t * المؤشر) ، فلا ، لا يمكن تغيير قيمة
الحجم () . الكتابة إلى كائنات النوع
uint32_t يمكن فقط تعديل الكائنات من النوع
uint32_t ، والمؤشرات المشاركة في حساب
الحجم () لها نوع مختلف (8).
ومع ذلك ، في حالة
uint8_t ، على الأقل على
المترجمين المشهورين (9) ، ستكون الإجابة هي: نعم ، من الناحية النظرية
قد تتغير قيمة
الحجم () ، نظرًا لأن
uint8_t هو اسم مستعار
للحرف غير
الموقَّع ، ويمكن للصفائف من النوع
char (و
char )
غير الموقعة الاسم المستعار مع أي نوع آخر . هذا يعني أنه ، وفقًا لبرنامج التحويل البرمجي ، يمكن للكتابة على
مؤشرات uint8_t تعديل محتويات ذاكرة غير معروفة الأصل في أي عنوان (10). لذلك ، يفترض أن كل عملية زيادة
v [i] ++ يمكن أن تغير قيمة
الحجم () ، وبالتالي فهي مضطرة لإعادة حسابها في كل تكرار للحلقة.
نعلم جميعًا أن الكتابة إلى الذاكرة التي أشار إليها
std :: vector لن تغير
حجمها () ، لأن هذا قد يعني أن المتجه نفسه قد تم تخصيصه بطريقة ما داخل كومة الذاكرة المؤقتة الخاصة به ، وهذا عمليًا مستحيل ومشابه لمشكلة الدجاج والبيض (11). لكن لسوء الحظ هذا غير معروف للمترجم!
ماذا عن بقية النتائج؟
حسنًا ، اكتشفنا أن الإصدار الذي يحتوي على
uint8_ أبطأ قليلاً من إصدار
uint32_t على gcc بمستوى
-O2 . ولكن لماذا تفسر الفرق الكبير - ما يصل إلى 8 مرات - على clang أو نفس gcc on
-O3 ؟
كل شيء بسيط هنا: في حالة
uint32_t ، يمكن لـ clang تنفيذ ناقل تلقائي للحلقة:
.LBB1_6: ; =>This Inner Loop Header: Depth=1 vmovdqu ymm1, ymmword ptr [rax + 4*rdi] vmovdqu ymm2, ymmword ptr [rax + 4*rdi + 32] vmovdqu ymm3, ymmword ptr [rax + 4*rdi + 64] vmovdqu ymm4, ymmword ptr [rax + 4*rdi + 96] vpsubd ymm1, ymm1, ymm0 vpsubd ymm2, ymm2, ymm0 vpsubd ymm3, ymm3, ymm0 vpsubd ymm4, ymm4, ymm0 vmovdqu ymmword ptr [rax + 4*rdi], ymm1 vmovdqu ymmword ptr [rax + 4*rdi + 32], ymm2 vmovdqu ymmword ptr [rax + 4*rdi + 64], ymm3 vmovdqu ymmword ptr [rax + 4*rdi + 96], ymm4 vmovdqu ymm1, ymmword ptr [rax + 4*rdi + 128] vmovdqu ymm2, ymmword ptr [rax + 4*rdi + 160] vmovdqu ymm3, ymmword ptr [rax + 4*rdi + 192] vmovdqu ymm4, ymmword ptr [rax + 4*rdi + 224] vpsubd ymm1, ymm1, ymm0 vpsubd ymm2, ymm2, ymm0 vpsubd ymm3, ymm3, ymm0 vpsubd ymm4, ymm4, ymm0 vmovdqu ymmword ptr [rax + 4*rdi + 128], ymm1 vmovdqu ymmword ptr [rax + 4*rdi + 160], ymm2 vmovdqu ymmword ptr [rax + 4*rdi + 192], ymm3 vmovdqu ymmword ptr [rax + 4*rdi + 224], ymm4 add rdi, 64 add rsi, 2 jne .LBB1_6
تم نشر الدورة 8 مرات ، وهذا بشكل عام هو الحد الأقصى للأداء الذي يمكنك الحصول عليه: ناقل واحد (8 عناصر) لكل دورة على مدار الساعة لذاكرة التخزين المؤقت L1 (لن يتوقف هذا العمل بسبب تقييد عملية الكتابة الواحدة لكل دورة ساعة (12)).
لا
يتم إجراء
النقل باستخدام uint8_t ، لأنه يعوقه الحاجة إلى حساب
الحجم () للتحقق من حالة الحلقة في كل تكرار. لا يزال سبب التأخر كما هو ، لكن التأخر أكبر بكثير.
يتم شرح أقل توقيت من خلال
التحويل التلقائي: يطبق gcc ذلك فقط على مستوى
-O3 ، ويتم تطبيق
clang على كل من
-O2 و
-O3 المستويات بشكل افتراضي. يقوم برنامج التحويل البرمجي gcc بمستوى - cc بإنشاء رمز أبطأ قليلاً من clang لأنه لا يقوم بتوسيع الحلقة التلقائية.
تصحيح الوضع
اكتشفنا ما هي المشكلة - كيف يمكننا حلها؟
أولاً ، دعنا نجرب إحدى الطرق التي لن تنجح ، وهي أننا سنكتب دورة اصطلاحية أكثر استنادًا إلى مكرر:
for (auto i = v.begin(); i != v.end(); ++i) { (*i)++; }
سيكون الرمز الذي ينشئه
مجلس التعاون الخليجي عند مستوى
O2 أفضل قليلاً من الخيار ذي
الحجم () :
.L17: add BYTE PTR [rax], 1 add rax, 1 cmp QWORD PTR [rdi+8], rax jne .L17
تحولت عمليتان إضافيتان للقراءة إلى واحدة ، لأنني أقارن الآن مع مؤشر
نهاية المتجه ، بدلاً من إعادة حساب
الحجم () ، وطرح مؤشر بدء المتجه من مؤشر النهاية. حسب عدد الإرشادات ، تم العثور على هذا الرمز مع
uint32_t ، نظرًا لأن عملية القراءة الإضافية المدمجة مع عملية المقارنة. ومع ذلك ، لم تختف المشكلة ولا يزال
ناقل الحركة التلقائي غير متاح ، لذلك لا يزال
uint8_t متخلفًا بشكل كبير عن
uint32_t - أكثر من 5 مرات على كلٍ من gcc و clang بالمستويات التي يتم توفير vectorization بها.
لنجرب شيئًا آخر. لن ننجح مرة أخرى ، أو بالأحرى سنجد طريقة
أخرى غير صالحة للعمل.
في هذا الإصدار ، نحسب
الحجم () مرة واحدة فقط قبل الحلقة ونضع النتيجة في متغير محلي:
for (size_t i = 0, s = v.size(); i < s; i++) { v[i]++; }
يبدو أن العمل؟ كانت المشكلة هي
الحجم () ، والآن طلبنا من المترجم الالتزام نتيجة
الحجم () للمتغير المحلي
s في بداية الحلقة ، كما أن المتغيرات المحلية ، كما تعلمون ، لا تتقاطع مع البيانات الأخرى. لقد فعلنا بالفعل ما لم يتمكن المترجم من فعله. والشفرة التي ستنشئها ستكون في الواقع أفضل (مقارنة بالأصل):
.L9: mov rdx, QWORD PTR [rdi] add BYTE PTR [rdx+rax], 1 add rax, 1 cmp rax, rcx jne .L9
هناك عملية قراءة إضافية واحدة فقط ولا يوجد أمر
فرعي . ولكن ما الذي يفعله هذا الأمر الإضافي (
rdx ، QWORD PTR [rdi] ) إذا لم يشارك في حساب الحجم؟ يقرأ مؤشر
البيانات () من
v !
يتم تطبيق التعبير
v [i] كـ
* (v.data () + i) ، والعضو الذي يتم إرجاعه بواسطة
البيانات () (وفي الواقع ، مؤشر
بدء منتظم) يؤدي إلى نفس المشكلة مثل
size () . صحيح ، لم ألاحظ هذه العملية في الإصدار الأصلي ، لأنه كان هناك "مجاني" ، لأنه لا يزال يتعين تنفيذها من أجل حساب الحجم.
تحمل مع أكثر من ذلك بقليل ، وجدنا حلا تقريبا. كل ما تحتاجه هو إزالة
كل التبعيات من محتويات
std :: vector من حلقة لدينا. أسهل طريقة للقيام بذلك هي تعديل تعبيرنا باستخدام أداة التكرار قليلاً:
for (auto i = v.begin(), e = v.end(); i != e; ++i) { (*i)++; }
الآن تغير كل شيء بشكل كبير (هنا نقوم بمقارنة الإصدارات فقط مع
uint8_t - في إحداها نقوم بحفظ
نهاية التكرار في متغير محلي
قبل الحلقة ، في الأخرى - لا):
أعطانا هذا التغيير الطفيف زيادة قدرها 20 مرة في السرعة عند المستويات مع ناقل الحركة التلقائي. علاوة على ذلك ، لم
تلحق الشفرة ذات
uint8_t فقط بالرمز مع
uint32_t - لقد تجاوزتها تقريبًا 4 مرات تقريبًا بواسطة gcc
-O3 و
clang -O2 و
-O3 ، كما توقعنا في البداية ، بالاعتماد على vectorization: في النهاية ، أربعة أضعاف بالضبط يمكن معالجة العناصر من خلال عملية متجهة ونحتاج إلى عرض نطاق ترددي أقل أربع مرات - بغض النظر عن مستوى ذاكرة التخزين المؤقت (13).
إذا قرأت هذا المكان ، فيجب عليك أن تصرخ لنفسك طوال هذا الوقت:
ولكن ماذا عن الحلقة الدائرية مع مقدمة الفرقة المقدمة في الإصدار C ++ 11؟أسارع لإرضائك: إنه يعمل! هذا ، في الواقع ، هو السكر النحوي ، الذي تختبئ خلفه نسختنا مع التكرار بنفس الشكل تقريبًا ، حيث قمنا بتثبيت مؤشر
النهاية في متغير محلي قبل بداية الحلقة. لذلك سرعته هي نفسها.
إذا قررنا فجأة العودة إلى أوقات الكهوف القديمة وكتابة وظيفة تشبه C ، فإن مثل هذا الرمز يعمل بنفس القدر:
void array_inc(uint8_t* a, size_t size) { for (size_t i = 0; i < size; i++) { a[i]++; } }
هنا ، يتم تمرير المؤشر إلى الصفيف
a ومتغير
الحجم إلى الدالة حسب القيمة ، لذلك لا يمكن تغييرهما كنتيجة للكتابة إلى المؤشر
a (14) - تمامًا مثل المتغيرات المحلية. أداء هذا الرمز هو نفس أداء الخيارات السابقة.
أخيرًا ، في برامج التحويل البرمجي حيث يتوفر هذا الخيار ، يمكنك التصريح عن متجه باستخدام
__restrict (15):
void vector8_inc_restrict(std::vector<uint8_t>& __restrict v) { for (size_t i = 0; i < v.size(); i++) { v[i]++; } }
الكلمة الأساسية __restrict ليست جزءًا من معيار C ++ ، ولكنها جزء من المعيار
C منذ C99 (حسب
التقييد ). إذا تم تطبيقه كملحق C ++ في المحول البرمجي ، فمن المرجح أن يطيع دلالات C. بالطبع ، لا توجد روابط في C ، لذلك يمكنك استبدال عقلية الارتباط إلى المتجه بمؤشر إلى المتجه.
لاحظ أن التقييد لا يحتوي على
خاصية متعدية: إجراء محدد
__restrict ، والذي يتم من خلاله الإعلان عن ارتباط
std :: vector ، ينطبق فقط على أعضاء المتجه نفسه ، وليس على منطقة الكومة المشار إليها بواسطة
v.data () . في حالتنا ، هناك حاجة إلى المزيد ، لأنه (كما في حالة المتغيرات المحلية) يكفي لإقناع المترجم أن المصطلحات نفسها ، مع الإشارة إلى بداية ونهاية المتجه ، لا تتقاطع مع أي شيء. ومع ذلك ، لا يزال شرط
التقييد ذا صلة ، لأن الكتابة عبر
v.data () قد لا تزال تتسبب في تغيير كائنات أخرى في
وظيفتك بسبب الاسم المستعار.
خيبة أمل
هنا نصل إلى الخاتمة الأخيرة - والمخيبة للآمال للغاية. الحقيقة هي أن جميع الحلول الموضحة أعلاه تنطبق فقط على هذه الحالة المحددة ، عندما يمكن للمتجه أن يتداخل نظريًا مع نفسه. كان الحل هو الخروج من الحلقة أو عزل نتيجة استدعاء
حجم () أو
نهاية () المتجه ،
وعدم إخبار المترجم بأن الكتابة إلى بيانات المتجه لا تؤثر على البيانات الأخرى. سيكون من الصعب قياس هذا الرمز مع نمو الوظيفة.
لم تختف مشكلة التعرجات ، ولا يزال بإمكان أوامر الكتابة "أي مكان" - ببساطة لا توجد بيانات أخرى في هذه الوظيفة يمكن أن تتأثر ... في الوقت الحالي. بمجرد ظهور رمز جديد فيه ، سيتم تكرار كل شيء. هنا
مثال مرتجل . إذا كتبت إلى صفائف عناصر من نوع
uint8_t في حلقات صغيرة ،
فيجب عليك القتال مع المترجم حتى النهاية (16).
تعليقات
سوف أكون سعيدا لأي ملاحظات. ليس لدي بعد نظام تعليق (17) ، لذلك ، كالعادة ، سنناقش في
هذا الموضوع على HackerNews .
- من خلال الوصول إلى الذاكرة هنا ، من المفهوم أن سلسلة التبعيات تمر عبر الذاكرة: يجب أن تقرأ أوامر الكتابة في العنوان نفسه القيمة الأخيرة المكتوبة هناك ، وبالتالي فإن هذه العمليات تعتمد (في الممارسة العملية ، سيتم إعادة توجيه التحميل (STLF) إذا كان التسجيل كافياً في كثير من الأحيان). يمكن أن تحدث تبعيات الأمر add عند الوصول إلى الذاكرة بطرق أخرى ، على سبيل المثال ، عن طريق حساب العنوان ، ولكن بالنسبة لحالتنا ، فإن هذا غير مناسب.
- تظهر فقط دورة صغيرة هنا ؛ رمز التثبيت بسيط ويعمل بسرعة. لرؤية القائمة الكاملة ، قم بتحميل الكود إلى godbolt .
- ربما ينبغي أن يسمى ببساطة "الحد الأدنى"؟ بصرف النظر عن ذلك ، لا يقوم برنامج التحويل البرمجي gcc عادةً بفصل الحلقات حتى عند مستويين O2 و -O3 ، إلا في حالات خاصة عندما يكون عدد التكرارات صغيرًا ويعرف في مرحلة التجميع . لهذا السبب ، تُظهر gcc نتائج اختبار أقل مقارنةً بـ clang ، ولكنها توفر الكثير على حجم الرمز. يمكنك فرض gcc لإلغاء تحديد الحلقات عن طريق تطبيق تحسين ملف التعريف أو عن طريق تشغيل علامة -funroll-loops .
- في الواقع ، فإن الأمر inc DWORD PTR [rax] في gcc هو تحسين مفقود : من الأفضل دائمًا استخدام الأمر add [rax] ، 1 ، نظرًا لأنه يتكون من 2 فقط من العمليات المصغرة المدمجة مقابل 3 لـ inc . في هذه الحالة ، يكون الفرق حوالي 6٪ فقط ، ولكن إذا تم توسيع الدورة بشكل طفيف بحيث تم تكرار عملية الكتابة فقط ، فسيكون الاختلاف كبيرًا (لن تؤدي زيادة أخرى دورًا ، نظرًا لأننا سنصل إلى الحد الأقصى 1 عملية التسجيل في كل دورة ، والتي لا تعتمد على العدد الإجمالي للعمليات الصغيرة).
- أسمي هذا المتغير الاستقرائي ، وليس فقط i ، كما في التعليمات البرمجية المصدر ، لأن المحول البرمجي حوّل عمليات وحدة الزيادة i إلى زيادات 4 بايت من المؤشر المخزّن في سجل rax ، وبالتالي قام بتصحيح حالة الحلقة. في شكله الأصلي ، تتناول الحلقة الخاصة بنا عناصر المتجه ، وبعد هذا التحويل ، يزيد المؤشر / التكرار ، وهو أحد الطرق لخفض تكلفة العمليات .
- في الواقع ، إذا قمت بتمكين -funroll-loops ، فستكون السرعة في gcc 1.08 لكل عنصر مع طرح 8x . ولكن حتى مع هذه العلامة ، لن يقوم بتوسيع حلقة الإصدار مع عناصر 8 بت ، لذلك سيكون التباطؤ في السرعة أكثر وضوحًا!
- هؤلاء الأعضاء لديهم معدل خاص ، وأسمائهم تعتمد على التطبيق ، لكن في stdlibc ++ لا يطلق عليهم حقًا البداية والنهاية ، كما في gcc. يطلق عليهم _Vector_base :: _ Vector_impl :: _ M_start و _Vector_base :: _ Vector_impl :: _ M_finish على التوالي ، أي أدخل بنية _Vector_impl ، والتي هي عضو في _M_impl ( والواحد فقط) من فئة _Vector_base ، وهذا بدوره هو الفئة الأساسية لل std :: vector . حسنا ، حسنا! لحسن الحظ ، فإن المترجم يتكيف بسهولة مع كومة التجريد هذه.
- لا يصف المعيار ما هي الأنواع الداخلية لأعضاء std :: vector ، ولكن في مكتبة libstdc ++ ، يتم تعريفهم ببساطة على أنهم Alloc :: pointer (حيث Alloc هو مخصص للناقل) ، وبالنسبة للكائن الافتراضي std :: المخصص الذي سوف يقومون ببساطة مؤشرات من النوع T * ، أي مؤشرات منتظمة إلى كائن - في هذه الحالة uint32_t * .
- أنا أبدي هذا التحفظ لسبب ما. هناك شك في أن uint8_t يمكن اعتباره نوعًا غير char ، char موقعة و char غير موقعة . نظرًا لأن الاسم المستعار يعمل مع أنواع الأحرف ، فإن هذا لا ينطبق من حيث المبدأ على uint8_t ويجب أن يتصرف مثل أي نوع آخر غير أحرف. ومع ذلك ، فإن أيا من المترجمين الذين أعرفهم يعتقدون ذلك: في كل منهم ، typedef uint8_t هو اسم مستعار للحرف غير الموقّع ، حتى لا يرى المترجمون الفرق بينهم ، حتى لو أرادوا استخدامه.
- بكلمة "أصل غير معروف" أعني هنا فقط أن المترجم لا يعرف أين تشير محتويات الذاكرة أو كيف ظهرت. يتضمن ذلك مؤشرات تعسفية تم تمريرها إلى الوظيفة ، وكذلك متغيرات عامة وثابتة. , , , , , ( - ). , malloc new , , , , : , , . , malloc new .
- , std::vector - ? , std::vector<uint8_t> a a.data() placement new b . std::swap(a, b) , – , b ? , b . : - (, ), , .
- 8 , .. 32 . , std::vector .
- - 4 : , , – . : 8- L1, 32- – L2 , .
- , – : . , , «».
- v[i] , .
- . , «» , uint8_t . , , , uint8_t , . , clang, gcc , , uint8_t . - gcc , . , , - __restrict .
- - , , ( Disqus), ( ), .
. : Travis Downs. Incrementing vectors .