واحد دافئة في أمسية شتوية باردة ، أردت الاحماء في المكتب واختبار نظرية أحد الزملاء بأن متجه C ++ يمكنه القيام بالمهمة بشكل أسرع من قائمة CPython.
في الشركة ، نقوم بتطوير منتجات قائمة على Django ، وقد حدث أنه كان من الضروري معالجة مجموعة كبيرة من القواميس. اقترح أحد الزملاء أن التنفيذ في C ++ سيكون أسرع بكثير ، لكنني لم أفقد الشعور بأن Guido والمجتمع ربما يكونان أكثر برودة قليلاً منا في C وربما قررنا بالفعل وتجاوزنا جميع المزالق ، وننفذ كل شيء بشكل أسرع بكثير.
لاختبار النظرية ، قررت أن أكتب ملف اختبار صغيرًا قررت فيه أن أعرض في حلقة إدخال قواميس 1 مليون من نفس المحتوى في المصفوفة ومتجه 100 مرة على التوالي.
النتائج ، على الرغم من أنها كانت متوقعة ، ولكن أيضا مفاجئة.
لقد حدث أننا نستخدم Cython بنشاط ، لذا بشكل عام ستختلف النتائج في تطبيق CPython بالكامل.
قف
- احسب Linux onegreyonewhite 4.18.14-calculate # 1 SMP PREEMPT Sat Oct 13 21:03:27 UTC 2018 x86_64 Intel® Core (TM) i7-4770 CPU @ 3.40GHz GenuineIntel GNU / Linux
- بيثون 2.7 و 3.6
- سيثون 0.28.3
- gcc (Gentoo 7.3.0-r3 p1.4)
النصي
بالمناسبة ، كان علي أن العبث هنا. للحصول على الأرقام الأكثر واقعية (أي ليس فقط جعلها فائقة الأمثل ، ولكن أيضًا حتى نتمكن من استخدامها لاحقًا دون الرقص مع الدف) ، كان علينا أن نفعل كل شيء في البرنامج النصي الرئيسي وتقليل جميع الإضافات الإضافية .h .
كانت المشكلة الأولى هي أن برنامج Cython wrapper for vector لا يريد أن يعمل مثل هذا:
لهذا كله ، لقد حصلوا على خطأ أنه من المستحيل إلقاء إملاء على PyObject. بالطبع ، هذه هي مشاكل Cython ، ولكن بما أننا نستخدمها ، فنحن بحاجة إلى حل هذه المشكلة المحددة.
اضطررت لصنع عكاز صغير في شكل
#include "Python.h" static PyObject * convert_to_pyobject(PyObject *obj) { return obj; }
الشيء المدهش هو أنه نجح. أكثر ما يخيفني هو أنني لا أفهم تمامًا لماذا وما يترتب على ذلك من عواقب.
المصادر النهائيةcython_experiments.h
#include "Python.h" static PyObject * convert_to_pyobject(PyObject *obj) { return obj; }
cython_experiments.pyx
محاولة 1
أريد حقًا أن أكون قادرًا على جمع * .whl للمشروع وأن كل شيء انتهى على أي نظام تقريبًا ، لذلك تم تعيين علامة التحسين أولاً على 0. وقد أدى ذلك إلى نتيجة غريبة:
Python 2.7 Statistics: attempts: 100 list avg time: 2.61709237576 vector avg time: 2.92562381506
بعد قليل من الانعكاس ، قررت أننا لا نزال نستخدم علامة -O1 ، لذلك قمت بتعيينها على النحو نفسه وحصلت عليها:
Python 2.7 Statistics: attempts: 100 list avg time: 2.49274396896 vector avg time: 0.922211170197
بطريقة ما ، شعرت بالضيق قليلاً: ومع ذلك ، خذني الإيمان بالكفاءة المهنية لشركة Guido and Co. لكن بعد ذلك ، لاحظت أن البرنامج النصي يأكل الذاكرة بطريقة مشبوهة بطريقة ما ، وفي النهاية كان يستهلك حوالي 20 جيجابايت من ذاكرة الوصول العشوائي. كانت المشكلة في هذا: في البرنامج النصي النهائي ، يمكنك مراقبة الوظيفة المجانية ، بعد اجتياز الحلقة. في هذا التكرار ، لم يكن بعد. ثم فكرت ...
ولكن هل يمكنني تعطيل جي سي؟
بين المحاولات ، قمت بإجراء gc.disable () وبعد محاولة gc.enable () . أبدأ التجميع والبرنامج النصي وأحصل على:
Python 2.7 Statistics: attempts: 100 list avg time: 1.00309731514 vector avg time: 0.941153049469
بشكل عام ، الفرق ليس كبيرًا ، لذلك اعتقدت أنه لم يكن هناك جدوى تبالغ حاول تحريف بطريقة ما واستخدم CPython فقط ، ولكن لا يزال جمعها مع Cython.
ربما يكون لدى العديد منهم سؤال: "ما الذي يوجد بالذاكرة؟" المدهش (لا) هو أن لا شيء. نمت بنفس المعدل وبنفس الكمية. جاء مقال إلى الذهن ، لكنني لم أرغب في الذهاب إلى مصادر بيثون على الإطلاق. نعم ، وهذا يعني شيئًا واحدًا فقط - المشكلة في تنفيذ المتجه.
النهائي
بعد الكثير من عذاب تحويل النوع ، أي أن المتجه يقبل مؤشر إلى القاموس ، تم الحصول على نفس البرنامج النصي الناتج مع تشغيل gc ، وحصلت على متوسط فرق قدره 2.6 مرة (الموجه أسرع) وأداء ذاكرة جيد نسبيًا.
فجأة اتضح لي أنني أجمع كل شيء فقط تحت Py2.7 ولم أحاول القيام بأي شيء مع 3.6.
وهنا فوجئت حقًا (بعد النتائج السابقة ، كانت المفاجأة منطقية):
Python 3.6 Statistics: attempts: 100 list avg time: 0.8771139788627624 vector avg time: 1.075702157020569 Python 2.7 Statistics: attempts: 100 list avg time: 2.61709237576 vector avg time: 0.92562381506
مع كل هذا ، لا يزال gc يعمل ، الذاكرة لم تلتهم ، وكان نفس البرنامج النصي. مع إدراك أنه بعد أكثر من عام بقليل ، سيكون من الضروري أن نقول وداعًا إلى 2.7 ، ما زلت أتساءل أن هناك فرقًا بينهما. في أغلب الأحيان ، سمعت / قرأت / جربت و Py3.6 كان أبطأ من Py2.7. ومع ذلك ، فإن اللاعبين من Cython-developers قاموا بشيء مذهل وغيروا الوضع في مهده.
ملخص
بعد هذه التجربة ، قررنا عدم الإزعاج كثيرًا بدعم Python 2.7 وإعادة تصنيع أي جزء من تطبيقات C ++ ، ببساطة لأنه لا يستحق ذلك. لقد تمت كتابة كل شيء بالفعل أمامنا ، لا يمكننا استخدامه إلا بشكل صحيح لحل مشكلة معينة.
UPD 12/24/2018:
بناءً على نصيحة iCpu وبعد الهجمات على الجانب ، تم التحقق من عدم فهم ماذا وكيف ، حاولت إعادة كتابة الجزء C ++ بالطريقة الأكثر ملاءمة للتطوير في المستقبل ، وكذلك تقليل التجريدات. اتضح ما هو أسوأ:
نتيجة ضعف المعرفة C ++cython_experiments.h
#include "Python.h" #include <vector> #include <algorithm> #ifndef PyString_AsString #define PyString_AsString PyUnicode_AsUTF8 #define PyString_FromString PyUnicode_FromString #endif typedef struct { char* name; bool reverse; } sortFiled; class cmpclass { public: cmpclass(std::vector<char*> fields) { for (std::vector<char*>::iterator it = fields.begin() ; it < fields.end(); it++){ bool is_reverse = false; char* name; if (it[0] == "-"){ is_reverse = true; for(int i=1; i<strlen(*it); ++i) name[i] = *it[i]; } else { name = *it; } sortFiled field = {name, is_reverse}; this->fields_to_cmp.push_back(field); } } ~cmpclass() { this->fields_to_cmp.clear(); this->fields_to_cmp.shrink_to_fit(); } bool operator() (PyObject* left, PyObject* right) { // bool result = false; for (std::vector<sortFiled>::iterator it = this->fields_to_cmp.begin() ; it < this->fields_to_cmp.end(); it++){ // PyObject* str_name = PyString_FromString(it->name); PyObject* right_value = PyDict_GetItem(right, str_name); PyObject* left_value = PyDict_GetItem(left, str_name); if(!it->reverse){ result = left_value < right_value; } else { result = (left_value > right_value); } PyObject_Free(str_name); if(!result) return false; } return true; } private: std::vector<sortFiled> fields_to_cmp; }; void vector_multikeysort(std::vector<PyObject *> items, PyObject* columns, bool reverse) { std::vector<char *> _columns; for (int i=0; i<PyList_GET_SIZE(columns); ++i) { PyObject* item = PyList_GetItem(columns, i); char* item_str = PyString_AsString(item); _columns.push_back(item_str); } cmpclass cmp_obj(_columns); std::sort(items.begin(), items.end(), cmp_obj); if(reverse) std::reverse(items.begin(), items.end()); } std::vector<PyObject *> _test_vector(PyObject* store_data_list, PyObject* columns, bool reverse = false) { int range_attempts = PyList_GET_SIZE(store_data_list); std::vector<PyObject *> data_list; for (int i=0; i<range_attempts; ++i) { data_list.push_back(PyList_GetItem(store_data_list, i)); } vector_multikeysort(data_list, columns, reverse); return data_list; }
cython_experiments.pyx
Python 3.6 Statistics: attempts: 10 list avg time: 0.2640914678573608 vector avg time: 2.5774293661117555
أي أفكار ما يمكن تحسينه في coparator بحيث يعمل بشكل أسرع؟