c++ - phenotype - phenotypic effects




أسرع تنفيذ بسيطة، الظاهري، مراقب نوع من، نمط في ج++؟ (2)

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

تشبه إلى حد ما حلقة غير محاذاة ، وهذا يحول حلقة واحدة استدعاء الأسلوب على كل كائن من أجل N حلقات (للأنواع المدعومة N) التي كل استدعاء الأسلوب على جميع الكائنات من نوع معين. هذا يتجنب التكلفة الأولية للإرسال الظاهري لا يمكن التنبؤ بها: فرع سوء التنبؤات التي تنطوي عليها الدعوة غير المباشرة من وظيفة غير معروفة وغير متوقعة في فابلت.

التنفيذ العام لهذه التقنية ينطوي على تمرير أول لتقسيم الكائنات حسب النوع: يتم استخدام المعلومات حول هذا القسم عن طريق تمرير الثاني الذي يحتوي على حلقات منفصلة لكل نوع 1 ، استدعاء الأسلوب. وهذا لا ينطوي عموما على أي فروع لا يمكن التنبؤ بها على الإطلاق، إذا ما نفذت بعناية.

في حالة اثنين من الطبقات المشتقة B و C يمكنك ببساطة استخدام صورة نقطية لتخزين المعلومات نوع. وإليك مثال لتنفيذ، باستخدام الأنواع A و B و C من الشفرة في السؤال:

void virtual_call_unswitch(std::vector<A*>& vec) {

    // first create a bitmap which specifies whether each element is B or C type
    std::vector<uint64_t> bitmap(vec.size() / 64);

    for (size_t block = 0; block < bitmap.size(); block++) {
        uint64_t blockmap = 0;
        for (size_t idx = block * 64; idx < block * 64 + 64; idx++) {
            blockmap >>= 1;    
            blockmap |= (uint64_t)vec[idx + 0]->typecode_ << 63;
        }
        bitmap[block] = blockmap;
    }

    // now loop over the bitmap handling all the B elements, and then again for all the C elements

    size_t blockidx;
    // B loop
    blockidx = 0;
    for (uint64_t block : bitmap) {
        block = ~block;
        while (block) {
            size_t idx = blockidx + __builtin_ctzl(block);
            B* obj = static_cast<B*>(vec[idx]);
            obj->Update();
            block &= (block - 1);
        }
        blockidx += 64;
    }

    // C loop
    blockidx = 0;
    for (uint64_t block : bitmap) {
        while (block) {
            size_t idx = blockidx + __builtin_ctzl(block);
            C* obj = static_cast<C*>(vec[idx]);
            obj->Update();
            block &= (block - 1);
        }
        blockidx += 64;
    }
}

هنا، typecode هو حقل شائع في A الذي يحدد نوع الكائن، 0 ل B و 1 ل C هناك حاجة إلى شيء مماثل لجعل التصنيف حسب النوع ممكنا (لا يمكن أن يكون مكالمة افتراضية، لأن إجراء مكالمة لا يمكن التنبؤ بها هو ما نحاول تجنبه في المقام الأول).

وهناك نسخة محسنة قليلا من ما سبق يظهر حول سرعة 3.5x للنسخة أونسويتشد عبر عادي أرسلت تقريبا حلقة، مع الإصدار الظاهري مسافة السباق في حوالي 19 دورات لكل إيفاد، والنسخة غير محجوب في حوالي 5.5. النتائج الكاملة:

-----------------------------------------------------------------------------
Benchmark                                      Time           CPU Iterations
-----------------------------------------------------------------------------
BenchWithFixture/VirtualDispatchTrue       30392 ns      30364 ns      23033   128.646M items/s
BenchWithFixture/VirtualDispatchFakeB       3564 ns       3560 ns     196712   1097.34M items/s
BenchWithFixture/StaticBPtr                 3496 ns       3495 ns     200506    1117.6M items/s
BenchWithFixture/UnswitchTypes              8573 ns       8571 ns      80437   455.744M items/s
BenchWithFixture/StaticB                    1981 ns       1981 ns     352397    1.9259G items/s

VirtualDispatchTrue هو Update() الدعوة حلقة بسيطة Update() على مؤشر من نوع A :

for (A *a : vecA) {
    a->Update();
}

VirtualDispatchFakeB يلقي المؤشر إلى B* (بغض النظر عن ما هو النوع الأساسي) قبل استدعاء Update() . منذ B::Update() نهائي، يمكن للمترجم تماما دي-فيرتواليز ومضمنة المكالمة. بطبيعة الحال، هذا لا يفعل الشيء الصحيح على الإطلاق: انها معالجة أي الكائنات C كما B وهكذا استدعاء الأسلوب الخاطئ (و وب تماما) - ولكن هنا لتقدير مدى السرعة التي يمكن استدعاء أساليب على متجه من مؤشرات إذا كان كل كائن نفس النوع المعروف بشكل ثابت.

for (A *a : vecA) {
    ((B *)a)->Update();
}

StaticBPtr يتكرر عبر std::vector<B*> بدلا من std::vector<A*> StaticBPtr . كما هو متوقع الأداء هو نفسه "وهمية B" التعليمات البرمجية أعلاه، حيث أن الهدف من Update() هو معروف بشكل ثابت وغير قابل تماما. انها هنا بمثابة الاختيار التعقل.

UnswitchTypes هو نوع خدعة غير مهيأة المذكورة أعلاه.

StaticB يتكرر عبر std::vector<B> . وهذا يعني أن الأجسام " B مخصصة بصورة متقاربة بدلا من متجه للمؤشرات إلى الأجسام "ب". هذا يزيل مستوى واحد من غير مباشر ويظهر شيئا مثل أفضل حالة لهذا الكائن تخطيط 2 .

المصدر الكامل متاح.

محددات

الآثار الجانبية والنظام

الحد الرئيسي مع هذه التقنية هو أن أمر Update() المكالمات لا يهم. بينما يتم Update() مرة واحدة على كل كائن، تم تغيير الترتيب بشكل واضح. طالما أن الكائن لا يقوم بتحديث أي حالة عالمية أو مشتركة قابلة للتحويل، يجب أن يكون هذا سهل الإرضاء.

يدعم لنوعين

يدعم الرمز أعلاه نوعين فقط، استنادا إلى استخدام الصورة النقطية لمعلومات نوع السجل.

هذا التقييد سهل الإزالة. أولا، يمكن توسيع نطاق النهج النقطي. على سبيل المثال، لدعم 4 أنواع، يمكن إنشاء اثنين من الصور النقطية مماثلة التي بت المقابلة من كل صورة نقطية أساسا لحقل 2 بت ترميز هذا النوع. الحلقات متشابهة، إلا أنه في الحلقة الخارجية أنها & و ~ الصور النقطية معا في الطرق التي على جميع أنواع 4. على سبيل المثال:

// type 1 (code 11)
for (size_t i = 0; i < bitmap1.size(); i++) {
        block = bitmap1[i] & bitmap2[i];
        ...
}


// type 2 (code 01)
for (size_t i = 0; i < bitmap1.size(); i++) {
        block = ~bitmap1[i] & bitmap2[i];
        ...
}

...

هناك نهج آخر هو عدم استخدام الصور النقطية على الإطلاق، ولكن ببساطة تخزين صفيف من الفهارس لكل نوع. يشير كل فهرس في صفيف إلى كائن من هذا النوع في الصفيف الرئيسي. أساسا انها 1-تمرير الجذر نوع على رمز النوع. هذا ربما يجعل نوع الفرز جزء أبطأ قليلا، ولكن يحتمل أن يسرع منطق حلقة التكرار ( x & (x - 1) و ctz الاشياء يختفي، على حساب غير مباشر آخر).

ثابت عدد الأنواع المدعومة

وتدعم الشفرة أعلاه عددا ثابتا من الأنواع المعروفة في وقت تجميع البيانات (أي B و C ). إذا تم إدخال نوع جديد، فإن الكود أعلاه إما كسر وسوف تفشل بالتأكيد لاستدعاء Update() على هذه الأنواع الجديدة.

ومع ذلك، فمن السهل لإضافة دعم لأنواع غير معروفة. ببساطة مجموعة جميع أنواع غير معروفة، ثم لتلك الأنواع فقط، القيام بإرسال الظاهري الكامل داخل حلقة (أي، استدعاء Update() مباشرة على A* ). سوف تدفع كامل الثمن، ولكن فقط لأنواع التي لم تدعم صراحة! وبهذه الطريقة، تقنية التجزئة عمومية آلية الإرسال الظاهري.

1 في الواقع، تحتاج فقط حلقة واحدة لكل مجموعة من الأنواع التي تشترك في نفس تنفيذ الطريقة الظاهرية، على الرغم من أن هذا قد يكون من الصعب تنفيذ بطريقة عامة لأن هذه المعلومات ليست متاحة بسهولة. على سبيل المثال إذا كان كل من الصنف Y و Z مستمدين من X ، ولكن لا يلغي تنفيذ بعض الطريقة الظاهرية من X ، يمكن معالجة كل من X و Y و Z بنفس الحلقة.

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

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

كيف يمكن تنفيذ نمط التعليمة البرمجية التالية بأقل قدر من عمليات إعادة التوجيه / العمليات؟

يجب أن يتم ذلك في C ++ القياسية، ما يصل إلى 17.

class A{
    virtual void Update() = 0; // A is so pure *¬*
};

class B: public A
{
    override void Update() final
    {
        // DO B STUFF
    }
}

class C: public A
{
    override void Update() final
    {
        // DO C STUFF
    }
}

// class...

int main()
{
    std::vector<A*> vecA{};

    // Insert instances of B, C, ..., into vecA

    for(auto a: vecA) // This for will be inside a main loop
        a->Update(); // Ridiculous amount of calls per unit of time

    // Free memory
}

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

بس: أنا أعرف هذا هو الأمثل الجزئي ... هيك، أنا بحاجة إلى نانو أو حتى بيكو تحسين هذا (المجازي يتحدث)، ولذا فإنني ببساطة تجاهل أي ردود النفعية التي قد تأتي.


وكما قال أول تعليق، لديك مشكلة زي هنا. فرز / إعادة ترتيب على ما يرام، وكان لديك العديد من الكائنات، وليس عددا كبيرا من فئات مختلفة، وليس هناك حاجة لدعم الأنواع التي التعليمات البرمجية لا يعرف عن في وقت تجميع. تعدد الأشكال + الميراث الظاهري هو خيار خاطئ .

بدلا من ذلك، استخدم N حاويات مختلفة، واحدة لكل نوع من الكائن، مع عدم وجود غير مباشر. السماح للمترجم مضمنة B::Update() إلى حلقة على جميع الكائنات B هو أفضل بكثير . (للحصول على المثال تافهة أدناه من زيادة عضو int ، وتحليل بلدي الأداء الساكن من النظر في أسم يضعه في حوالي 24X أسرع على سكيليك مع البيانات الساخنة في ذاكرة التخزين المؤقت L1D AVX2 السيارات فيكوريزاتيون مقابل call في حلقة هو حقا أن ضخم.)

إذا كان هناك بعض النظام المطلوب بين الكائنات، بما في ذلك بين أنواع مختلفة من الكائنات، ثم نوع من تعدد الأشكال أو إيفاد اليدوي سيكون مناسبا. (على سبيل المثال إذا كان الأمر يتعلق بالترتيب الذي قمت بمعالجته vecA في حفظ جميع الكائنات B المنفصلة عن جميع الكائنات C لن يكون مكافئا.)

إذا كنت تهتم بالأداء، عليك أن تدرك أن جعل المصدر أكبر قد تبسيط الأمور للمترجم / في الإخراج أسم. فحص / إيفاد استنادا إلى نوع كل كائن داخل الحلقة الداخلية مكلفة. باستخدام أي نوع من وظيفة مؤشر أو إنوم لإيفاد على أساس لكل كائن يمكن أن تعاني بسهولة من ميسبريديكتس فرع عندما يكون لديك مزيج من كائنات مختلفة.

حلقات بشكل منفصل على حاويات متعددة يرفعون بشكل فعال هذا النوع تحقق من الحلقة الداخلية ويتيح مترجم ديفيرتواليز. (أو أفضل، يتقلص كل كائن لا يحتاج إلى مؤشر فالتابل، إنوم، أو مؤشر وظيفة في المقام الأول، لأن نوعه هو معروف بشكل ثابت.)

كتابة حلقة منفصلة لكل حاوية مع نوع مختلف هو نوع مثل تماما لفك حلقة على أنواع مختلفة بعد رفع نوع إيفاد خارج الحلقة الداخلية. هذا ضروري للمترجم لتضمين المكالمات، التي تريد إذا كان هناك الكثير من الكائنات من كل نوع. إنلينينغ يتيح لها الحفاظ على الثوابت في سجلات عبر الكائنات، وتمكن سيمد السيارات فيكوريزاتيون عبر كائنات متعددة، وببساطة يتجنب النفقات العامة لدعوة وظيفة الفعلية. (كل من المكالمة نفسها وانسكاب / إعادة تحميل السجلات).

كنت على حق أنه إذا كنت بحاجة إلى إرسال كل كائن ، C ++ وظائف افتراضية هي وسيلة مكلفة للحصول عليه عندما كنت تستخدم تجاوز final . كنت تدفع نفس تكلفة التشغيل التي من شأنها أن تسمح التعليمات البرمجية الخاصة بك دعم فئات مشتقة جديدة من حجم التعسفي الذي لم يكن يعرف عن في وقت تجميع، ولكن لا تحصل على أي فائدة من ذلك.

إن الإرسال الافتراضي يعمل فقط مع مستوى غير مباشر (على سبيل المثال ناقلات المؤشرات التي تستخدمها)، مما يعني أنك بحاجة إلى إدارة الأشياء المدببة إلى حد ما، على سبيل المثال من خلال تخصيصها من vector<B> poolB vector<C> poolC . على الرغم من أنني لست متأكدا من معظم تطبيقات vector<> استخدام realloc() عندما تحتاج إلى النمو؛ لا يحتوي أبي new/delete على realloc ، لذلك vector قد نسخ في الواقع في كل مرة ينمو، بدلا من محاولة توسيع التخصيص الموجود في المكان. تحقق من تنفيذ C ++ الخاص بك، لأنه قد تمتص مقارنة بما يمكنك القيام به مع مالوك / ريالوك.

و راجع للشغل، ينبغي أن يكون من الممكن أن تفعل new / delete مع راي مع عدم وجود النفقات العامة إضافية لتخصيص / ديالوكاتيون، طالما أن جميع الطبقات الخاصة بك هي تدميري تافهة. (ولكن لاحظ أن unique_ptr قد هزيمة التحسينات الأخرى لاستخدام ناقلات المؤشرات). std::unique_ptr يحذر من انها وب لتدميرها عن طريق مؤشر إلى فئة القاعدة، لذلك قد تضطر إلى لفة بنفسك. ومع ذلك، في غك على x86-64، sizeof(unique_ptr<class C>) هو 8 فقط، لذلك لديه عضو مؤشر واحد فقط. ولكن أيا كان، فإن تخصيص زيليونس من الأشياء الصغيرة تمتص بشكل منفصل حتى لا تفعل ذلك بهذه الطريقة في المقام الأول .

إذا كنت بحاجة إلى نوع من الإرسال مثل العنوان يطلب

إذا كانت الكائنات كلها أحجام مماثلة، ثم كنت حقا تريد حلقة فوق الكائنات، وليس مؤشرات إلى الكائنات . وهذا من شأنه تجنب البصمة الإضافية المخبأ لناقلات المؤشرات، ويتجنب الكمون الإضافي مطاردة الكمون أن خارج النظام التنفيذ يجب إخفاء لإبقاء وحدات التنفيذ مشغول. ولكن C ++ الوراثة الظاهرية لا توفر أي طريقة متوافقة مع المعايير للحصول على تعدد الأشكال union upoly { B b; C c; } poly_array[1024]; union upoly { B b; C c; } poly_array[1024]; يمكنك الإختراق هذا نفسك مع reinterpret_cast<> بطريقة ربما يعمل على x86-64 غك، ولكن ربما لا ينبغي. انظر @ بيونروب متابعة: تخزين متجاورة من أنواع متعددة الأشكال . (أيضا سؤال وجواب أقدم: C ++ تعدد الأشكال لكائن في صفيف ).

إذا كنت بحاجة إلى ذلك، فإن الطريقة الأعلى أداء ربما تكون لبناء بنفسك مع enum لفهرسة جدول مؤشرات وظيفة (أو استخدام switch() إذا وظائفك يمكن مضمنة). إذا لم تكن وظائفك مضمنة، فإن switch() إلى حفنة من case دالة المكالمة لا يحسن عادة إلى جدول لمؤشرات الدالة حتى لو كان لديهم جميعا نفس الأرجل (أو لا يجادل). عادة ما تحصل على جدول القفز إلى كتلة من تعليمات المكالمة، بدلا من القيام في الواقع call غير مباشرة. لذلك هناك قفزة إضافية في كل إيفاد.

C ++ 17 std::visit مع std::variant<B, C> (باستخدام الميراث غير الظاهري ل B و C) ويبدو أن تعطيك الإرسال على أساس الداخلية enum . std::visit يستخدم جدول القفز الخاصة بها لإرسال، حتى مع فقط 2 أنواع ممكنة، بدلا من تضمين لهم على حد سواء واستخدام فرع الشرطي. كما أن لديها للتحقق من حالة "غير مهيأ" في كل وقت. يمكنك الحصول على شفرة جيدة إذا كنت تعمل يدويا حول ذلك مع B *tmp = std::get_if<B>(&my_variant) ، و __builtin_unreachable() أن أقول غك أن نولبر ليس احتمالا. ولكن عند هذه النقطة قد كنت كذلك مجرد لفة الخاص بك struct polymorph { enum type; union { B b; C c; }; }; struct polymorph { enum type; union { B b; C c; }; }; (مع وظائف غير افتراضية) إذا كنت لا تحتاج إلى حالة "غير مهيأ". ذات صلة: C ++ تعدد الأشكال من كائن في مصفوفة .

في هذه الحالة لديك وظيفة واحدة فقط، حتى تتمكن من وضع مؤشر وظيفة داخل كل كائن كعضو . مثل void (*m_update)(A* this_object) . في المتصل، قم بتمرير مؤشر إلى الكائن باعتباره void* أو A* ، لأنها وظيفة غير عضو. سوف reinterpret_cast<C*>(this_object) تنفيذ وظيفة reinterpret_cast<C*>(this_object) . (لا dynamic_cast : نحن نفعل dynamic_cast الخاصة بنا، وليس استخدام C ++ لاعبالزبون).

إذا كنت ترغب في استخدام B و C في سياقات أخرى حيث يقوم عضو مؤشر الوظيفة بالتقاط مساحة بدون فائدة، يمكنك الاحتفاظ بمؤشرات الدالة في حاوية منفصلة بدلا من الطبقة الأساسية . لذلك سيكون for(i=0..n) funcptrs[i]( &objects[i] ); . طالما أن حاويات الخاص بك لا تخرج من المزامنة، وكنت دائما تمرير مؤشر إلى وظيفة أن يعرف ما يجب القيام به معها. استخدام ذلك مع union {B b; C c} objects[] union {B b; C c} objects[] (أو vector<union> ).

يمكنك استخدام void* إذا كنت تريد، خاصة إذا قمت بإجراء مجموعة منفصلة من مؤشرات الوظائف. ثم لا يحتاج أعضاء النقابة إلى الإرث من قاعدة مشتركة.

يمكنك استخدام std::function<> لتخزين مؤشرات إلى وظائف الأعضاء المثال، ولكن على x86-64 غك هذا كائن 32 بايت. فمن الأفضل لذاكرة التخزين المؤقت الخاصة بك فقط استخدام 8 بايت مؤشرات وظيفة العادية وكتابة التعليمات البرمجية التي تعرف لتمرير مؤشر صريح يعادل this المؤشر.

قد يستغرق وضع مؤشر الدالة في كل كائن مساحة أكبر من uint8_t أو uint8_t ، اعتمادا على الحجم الحالي / المحاذاة. قد يؤدي فهرس عدد صحيح صغير إلى جدول مؤشرات الوظائف إلى تقليل حجم كل مثيل من الكائنات الخاصة بك مقابل عضو المؤشر، خاصة بالنسبة إلى أهداف 64 بت. الأجسام الصغيرة يمكن أن يكون بسهولة يستحق الزوجين تعليمات إضافية لفهرسة صفيف من مؤشرات وظيفة، وربما أعلى عقوبة ميسبريديكت من ديريفيرانس مؤشر إضافي. ذاكرة / ذاكرة التخزين المؤقت ميسس غالبا ما يكون عنق الزجاجة.

أنا أفترض أن لديك بعض الدولة في حالة المثيل، على الرغم من أنك لا تظهر أي. إن لم يكن، ثم ناقلات مؤشرات وظيفة عادية إلى وظائف غير الأعضاء سيكون أرخص بكثير!

المقارنة العلوية:

كان لي إلقاء نظرة على أسمر ولدت أسم (غك والرنة استهداف x86-64) لبضعة طرق للقيام بذلك.

مصدر لطرق متعددة للقيام بذلك + أسم من x86-64 العصابة 5.0 على مستكشف المترجم غودبولت . يمكنك الوجه أكثر من غك، أو غير x86 أبنية.

class A{
    public:
    virtual void Update() = 0; // A is so pure *¬*
};

struct C : public A {
    int m_c = 0;
    public:
    void Update() override final
    {  m_c++;  }
};
int SC = sizeof(C);  // 16 bytes because of the vtable pointer

C global_c;  // to instantiate a definition for C::Update();

// not inheriting at all gives equivalent asm to making Update non-virtual
struct nonvirt_B //: public A
{
    int m_b = 0;
    void Update() //override final
    {  m_b++;  }
};
int SB = sizeof(nonvirt_B);  // only 4 bytes per object with no vtable pointer

void separate_containers(std::vector<nonvirt_B> &vecB, std::vector<C> &vecC)
{
    for(auto &b: vecB)        b.Update();
    for(auto &c: vecC)        c.Update();   
}

و غك لصناعة السيارات vecB حلقة على vecB مع vecB لمعالجة 8 عناصر int بالتوازي، لذلك إذا كنت لا عنق الزجاجة على عرض النطاق الترددي الذاكرة (أي الساخنة في ذاكرة التخزين المؤقت L1D)، هذه الحلقة يمكن زيادة 8 عناصر لكل دورة على مدار الساعة. يتم تشغيل هذه الحلقة بأسرع حلقة على vector<int> ؛ كل شيء إنلينس ويحسن بعيدا وانها مجرد زيادة المؤشر.

يمكن vecC فوق vecC أن تفعل فقط عنصر واحد لكل دورة على مدار الساعة ، لأن كل كائن هو 16 بايت (8 بايت فتابل مؤشر، 4 بايت int m_c ، 4 بايت من الحشو إلى حدود المحاذاة التالية لأن المؤشر يحتوي على شرط المحاذاة 8B.) دون final ، المجمع أيضا يتحقق مؤشر فالتابل لمعرفة ما اذا كان في الواقع C قبل استخدام C::Update() المضمنة C::Update() ، وإلا فإنه يرسل. انها مثل ما كنت تحصل على حلقة عبر struct { int a,b,c,d; } vecC[SIZE]; struct { int a,b,c,d; } vecC[SIZE]; دوينغ vecC[i].c++;

final يسمح ديفيرتواليزاشيون الكامل، ولكن يتم خلط البيانات لدينا مع مؤشرات فابل، لذلك المجمعين فقط تفعل العددية add [mem], 1 التي يمكن تشغيل فقط في 1 لكل ساعة (عنق الزجاجة على 1 في كل ساعة تخزين الإنتاجية بغض النظر عن حجم المخزن إذا انها ساخنة في مخبأ L1D). هذا في معظم الأحيان هزيمة سيمد لهذا المثال. (مع -march=skylake-avx512 ، غك -march=skylake-avx512 تفعل بعض خلط مثير للسخرية أو جمع / مبعثر وهذا أبطأ حتى من العددية، بدلا من مجرد تحميل / استعادة الكائن كله وإضافة مع المتجهات التي تغير فقط عضو int ، وهذا يسمح لأن فإنه لا يحتوي على أي أعضاء متقلبة أو ذرية، وسوف تشغيل 2 في كل ساعة مع AVX2، أو 4 في الساعة مع AVX512.) وجود الكائنات الخاصة بك تصل إلى 12 بايت أكبر هو الجانب السلبي الكبير إذا كانت صغيرة وكان لديك الكثير منهم.

مع أعضاء متعددة لكل كائن، وهذا لا يعني بالضرورة هزيمة سيمد، لكنه لا يزال يكلف الفضاء في كل كائن، تماما مثل مؤشر إنوم أو وظيفة.

منذ أن ذكرت نظرية محور فصل ، وآمل أن كنت لا تخطط لتخزين float x,y أزواج في كل كائن. صفيف من الهياكل يمتص أساسا ل سيمد، لأنه يحتاج إلى الكثير من خلط لاستخدام x مع y لنفس الكائن . ما تريد هو std::vector<float> x, y أو ما شابه ذلك، بحيث وحدة المعالجة المركزية الخاصة بك يمكن تحميل 4 x القيم في سجل و 4 y القيم في سجل آخر. (أو 8 في وقت واحد مع أفكس).

انظر الشرائح: سيمد في إنسومنياك ألعاب (غك 2015) لمقدمة لكيفية هيكلة البيانات الخاصة بك ل سيمد، وبعض الاشياء أكثر تقدما. انظر أيضا علامة ويكي سس لمزيد من الأدلة. أيضا، x86 العلامة ويكي لديها الكثير من مستوى منخفض x86 تحسين المواد. حتى لو لم تقم بتغيير أي شيء يدويا، مع صفائف منفصلة ل x و y هناك فرصة جيدة أن المحول البرمجي يمكن لصناعة السيارات في فيكتوريز لك. (انظر إلى الناتج أسم، أو المعيار gcc -O3 -march=native مقابل gcc -O3 -march=native -fno-tree-vectorize ). قد تحتاج -ffast-math لبعض أنواع فب المتجهات.

C ++ الإرسال الظاهري:

كتابة ذلك بالطريقة التي تفعلها في السؤال، مع الميراث الظاهري و

std::vector<A*> vecA{};

void vec_virtual_pointers() {
    for(auto a: vecA)
        a->Update();
}

نحصل على هذه الحلقة من clang5.0 -O3 -march=skylake

   # rbx = &vecA[0]
.LBB2_1:                         # do{
    mov     rdi, qword ptr [rbx]   # load a pointer from the vector (will be the this pointer for Update())
    mov     rax, qword ptr [rdi]   # load the vtable pointer
    call    qword ptr [rax]        # memory-indirect call using the first entry in the vtable
    add     rbx, 8                 # pointers are 8 bytes
    cmp     r14, rbx
    jne     .LBB2_1              # }while(p != vecA.end())

وبالتالي فإن مؤشر وظيفة النهائي هو في نهاية سلسلة من ثلاثة الأحمال تعتمد. التنفيذ خارج النظام يتيح هذا التداخل بين التكرارات (إذا كان الفرع يتنبأ بشكل صحيح)، ولكن هذا هو الكثير من النفقات العامة فقط في مجموع التعليمات للواجهة الأمامية، وكذلك في عقوبة سوء التفاهم. ( call [m] هو 3 عفوا، حتى مجرد حلقة نفسها هي 8 عفوا، ويمكن أن تصدر فقط واحد لكل 2 دورات على سكيليك.النداء / العودة لديها النفقات العامة جدا.إذا كان المتداعين ليس تافهة تماما، ونحن ربما لا عنق الزجاجة على إعادة توجيه المتجر للدفع / ظهور عنوان الإرجاع حلقة مع وظيفة استدعاء أسرع من حلقة فارغة (لست متأكدا من الإنتاجية من عمليات تخزين / إعادة تحميل مستقلة على نفس العنوان، وهذا يتطلب عادة إعادة تسمية الذاكرة، والتي سكيليك لا تفعل، لا عنق الزجاجة على أنه إذا كان المتدرب صغير مثل هنا.)

تعريف الكلانج ل C :: تحديث () هو

C::Update():                         # @C::Update()
    inc     dword ptr [rdi + 8]
    ret

إذا كان هذا يحتاج إلى إعداد أي الثوابت قبل حساب شيء، فإنه سيكون أكثر تكلفة لعدم أن يكون مضمنا. لذلك، مع إيفاد الظاهري، وهذا ربما يعمل في حوالي واحد في 3 إلى 5 ساعات، بدلا من حوالي 1 عضو لكل ساعة، على سكيليك. (أو 8 أعضاء لكل ساعة مع AVX2 لغير الظاهري class B التي لا تضيع الفضاء، ويجعل لصناعة السيارات في فيكوريزاتيون تعمل بشكل جيد.) http://agner.org/optimize/ يقول سكيليك لديه واحد في 3 3 إنتاجية ساعة على مدار الساعة، لذلك دعونا نقول فقدان الأداء 24X عندما تكون البيانات الساخنة في ذاكرة التخزين المؤقت L1D. سوف ميكروارشيتتوريس مختلفة تكون مختلفة، بطبيعة الحال. انظر علامة x86 ويكي لمزيد من المعلومات x86 بيرف.

الإختراق الاتحاد:

ربما يجب عليك أبدا استخدام هذا، ولكن يمكنك أن ترى من أسم أنه سوف تعمل على x86-64 مع عصابة و غك. أنا جعلت مجموعة من النقابات، وحلقت فوق ذلك:

union upoly {
    upoly() {}   // needs an explicit constructor for compilers not to choke
     B b;
     C c;
} poly_array[1024];

void union_polymorph() {
    upoly *p = &poly_array[0];
    upoly *endp = &poly_array[1024];
    for ( ; p != endp ; p++) {
        A *base = reinterpret_cast<A*>(p);
        base->Update();           // virtual dispatch
    }
}

أب و C كل منهم فتابل في البداية، لذلك أعتقد أن هذا سوف تعمل بشكل عام. نحن أسم هذا هو في الأساس نفسه، مع خطوة واحدة أقل من مطاردة المؤشر. (استخدمت مجموعة ثابتة بدلا من متجه، لأنني كنت حفظ الأشياء بسيطة و C- مثل في حين فرز ما يلقي.)

    lea     rdi, [rbx + poly_array]       ; this pointer
    mov     rax, qword ptr [rbx + poly_array]   ; load it too, first "member" is the vtable pointer
    call    qword ptr [rax]
    add     rbx, 16                       ; stride is 16 bytes per object
    cmp     rbx, 16384                    ; 16 * 1024
    jne     .LBB4_1

هذا هو أفضل، ويمس ذاكرة أقل، لكنه فقط أفضل قليلا للنفقات العامة.

std::function فروم #include <functional>

يمكن أن تعقد أي نوع من شيء قابل للاستدعاء. ولكن لديها المزيد من النفقات العامة من إرسال فابلت، لأنه يسمح لها أن تكون في حالة خطأ-إذا المستخدمة. لذلك الحلقة الداخلية لديها للتحقق من كل مثيل لذلك، وفخ إذا كان. أيضا، sizeof(std::function<void()>); هو 32 بايت (على x86-64 نظام V أبي).

#include <functional>
// pretty crappy: checks for being possibly unset to see if it should throw().
std::vector<std::function<void()>> vecF{};
void vec_functional() {
    for(auto f: vecF)     f();
}

                                # do {
.LBB6_2:                                # =>This Inner Loop Header: Depth=1
    mov     qword ptr [rsp + 16], 0       # store a 0 to a local on the stack?
    mov     rax, qword ptr [rbx + 16]
    test    rax, rax
    je      .LBB6_5           # throw on pointer==0  (nullptr)
    mov     edx, 2            # third arg:  2
    mov     rdi, r14          # first arg: pointer to local stack memory (r14 = rsp outside the loop)
    mov     rsi, rbx          # second arg: point to current object in the vector
    call    rax               # otherwise call into it with 2 args
    mov     rax, qword ptr [rbx + 24]    # another pointer from the std::function<>
    mov     qword ptr [rsp + 24], rax    # store it to a local
    mov     rcx, qword ptr [rbx + 16]    # load the first pointer again
    mov     qword ptr [rsp + 16], rcx
    test    rcx, rcx
    je      .LBB6_5           # check the first pointer for null again (and throw if null)
    mov     rdi, r14
    call    rax               # call through the 2nd pointer
    mov     rax, qword ptr [rsp + 16]
    test    rax, rax
    je      .LBB6_12          # optionally skip a final call
    mov     edx, 3
    mov     rdi, r14
    mov     rsi, r14
    call    rax
.LBB6_12:                               #   in Loop: Header=BB6_2 Depth=1
    add     rbx, 32
    cmp     r15, rbx
    jne     .LBB6_2

.LBB6_13:                       # return
    add     rsp, 32
    pop     rbx
    pop     r14
    pop     r15
    ret

.LBB6_5:
    call    std::__throw_bad_function_call()
    jmp     .LBB6_16
    mov     rdi, rax
    call    __clang_call_terminate

لذلك هناك ما يصل إلى ثلاثة تعليمات call ما لم يكن المؤشر نولبتر. هذا يبدو أسوأ بكثير من الإرسال الظاهري.

يبدو قليلا مختلفة مع عصابة -stdlib=libc++ ، بدلا من libstdc++ الافتراضي libstdc++ . ( https://libcxx.llvm.org/ ). ولكن لا يزال ثلاثة تعليمات call في الحلقة الداخلية، مع الشروط لتخطيها أو رمي.

ما لم يكن رمز-جن مختلفة جدا لأنواع مختلفة من function<T> ، فإنه ربما لا يستحق حتى النظر في ذلك للحصول على مؤشرات لوظائف الأعضاء إذا كنت تستطيع كتابة بديل أكثر كفاءة.







micro-optimization