البرمجة غير المتجانسة ومجموعة أدوات oneAPI. محاضرة خبير من إنتل تجيب على أسئلتك



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

صورة السؤال Soarex16
ما مدى صعوبة الانتقال من OpenCL إلى oneAPI وما الفوائد التي يمكن الحصول عليها من هذا؟

الجواب. يمكن أن يكون التبديل إلى DPC ++ أمرًا صعبًا ، ولكن في رأيي ، الأمر يستحق ذلك. هناك مرحلتان رئيسيتان.

أولاً ، هذا الانتقال من لغة البرمجة غير المتجانسة الخاصة بك (OpenCL ، حساب Vulkan) ، والتي ، على الأرجح ، تعتمد على API. هنا لديك بداية في حقيقة أنك تعرف بالفعل موضوع الموضوع ، والصعوبة في تحويل التفكير من التحكم المباشر عبر واجهة برمجة التطبيقات إلى بنيات لغة ضمنية أكثر قليلاً.
ثانياً ، هذا انتقال من اللغة المضيفة. إذا كنت تقوم بإلغاء تحميل حياتك بأكملها من C الخالص ، فإن عتبة الإدخال تساوي الحد الأدنى للتبديل من C إلى C ++ ، وهو معدل مرتفع للغاية.

لماذا تحاول؟

أولاً ، يقوم DPC ++ بعمل رائع للمبرمج. سوف تنسى بسرعة ، مثل كابوس ، كل هذه المكالمات الصريحة إلى clXXXYYY ، وما تعنيه الحجة السادسة ، وما إذا كنت قد نسيت رمز الإرجاع. تخفي العديد من الأغلفة الموجهة للكائنات الروتين بشكل أسوأ ، ولكن عادةً على حساب التبديل من OpenCL API القياسي إلى API المجمع غير القياسي (رأيت تلك الدراجات أيضًا). في حالة DPC ++ ، يمكنك ببساطة كتابة SYCL القياسي مع ملحقات Intel (والتي قد تصبح قريبًا SYCL القياسية أيضًا).

ثانياً ، يوفر DPC ++ تجميعًا مفصلًا ، أي أنه يمكنك التأكد من الأنواع ولن تواجه مشكلات على حدود واجهة برمجة التطبيقات (API) مع الأبعاد والحشو والمحاذاة. تكتب رمز kernel والمضيف في ملف واحد ، وهذا هو نفس الرمز. باستخدام USM ، يمكنك أيضًا العمل مع هياكل البيانات المعقدة بسهولة أكبر.

ثالثًا ، DPC ++ حقيقي C ++ ، أي أنه يسمح بالبرمجة المعممة. على سبيل المثال ، أبسط نواة لإضافة متجهين:

auto kern = [A, B, C](cl::sycl::id<1> wiID) { C[wiID] = A[wiID] + B[wiID]; //   A, B  C?  ! }; 

نفس الشيء على OpenCL:

 _kernel void vector_add(__global int *A, __global int *B, __global int *C) { int i = get_global_id(0); C[i] = A[i] + B[i]; } 

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

وجميع الفوائد لا تقتصر على هذا. سأذكر شيئًا آخر في الإجابات التالية.

لذلك قمت بتنزيل برنامج التحويل البرمجي في مكانك وجربته ، لأنه ليس من الصعب القيام بذلك مع حزمة OneAPI .

صورة سؤال جستر
هل ستكون OpenVINO و oneAPI مرتبطة بطريقة أو بأخرى؟

الجواب. أصبح توزيع OpenVINO الآن جزءًا من توزيع OneAPI. تعلم واستخدام الشبكات العصبية هي مهام صعبة حسابيا التي تستفيد كثيرا من البرمجة غير المتجانسة. أعتقد أنه عاجلاً أم آجلاً ، ستمكّن جميع مكونات OneAPI من استخدام جميع موارد الحوسبة المتاحة لك: مسرعات الرسومات والمعجلات الخاصة مثل Nervana و FPGA. وكل هذا دون ترك نموذج اللغة ونوع نظام برنامج C ++ الخاص بك.

صورة أسئلة من البريد
أحاول أن أفهم كيف سيبدو مسرع الأجهزة AI خلال 3 سنوات ، يرجى المساعدة في هذا. هناك شركة مثيرة للاهتمام Graphcore و IPU الخاص به - هذا الجهاز لا يقل كفاءة عن FPGA ، ولكن من الأسهل بكثير البرمجة - Python مع دعم TensorFlow والأطر الأخرى. اتضح أنه إذا تم الوفاء بوعود Graphcore ، فلن تكون هناك حاجة إلى FPGAs في سوق التعلم الآلي. بيثون هو أكثر ملاءمة بكثير ل dascascientists من C ++.
هل توافق على أن FPGA غير مناسب لسوق التعلم الآلي مقارنة بحلول Python القابلة للبرمجة؟ إذا تم فقدان سوق ML ، فما تطبيقات FPGA الواسعة الأخرى التي تراها؟
في أي تطبيقات ترى الحاجة الحتمية للبرمجة غير المتجانسة ، حيث لا يمكنك الحصول عليها من خلال أدوات أكثر ملاءمة مثل بيثون؟

الجواب. نظرت لفترة وجيزة إلى أي نوع من IPU. قطعة واحدة أخرى من الحديد والتي سوف تفريغ الجميع. هؤلاء الرجال يتنافسون على GPU ومع المعجلات الخاصة ، وليس مع FPGA.

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

افترض الآن أن IPU المحدد رائع حقًا. هذا لن يلغي البرمجة غير المتجانسة ، على العكس من ذلك ، فإن وجود مثل هذا المعجل الممتاز سوف يحفز عليه. كما أنه سيؤدي إلى بدء OneAPI و DPC ++ بشكل كبير ، لأن شخصًا ما عاجلاً أم آجلاً سيقول "أريد استخدام كلاً من IPU و GPU من برنامج واحد." مبكرًا لأن البرمجة غير المتجانسة تدور حول ذلك. معناها هو تفريغ مهمة مناسبة لجهاز مناسب. مهمة يمكن أن تأتي من أي مكان. وهذا الجهاز يمكن أن يكون أي شيء ، بل يمكن أن يكون هو نفس الجهاز الذي يعمل عليه البرنامج. على سبيل المثال ، إذا قمت بإلغاء تحميل النواة المكتوبة في ISPC واستخدمت ميزات ناقلات Xeon إلى الحد الأقصى ، فيمكنك إلغاء تحميلها بنفسك وتظل مكسبًا كبيرًا. المعيار الرئيسي هنا هو الأداء. حسنًا ، لن يكون هناك الكثير من الإنتاجية في هذا العالم. حتى مع أفضل المعجلات في العالم.

بالنسبة لبيثون وراحته ... يجب أن أعترف على الفور أنني لا أحب اللغات المكتوبة ديناميكيًا: فهي بطيئة وبدلاً من خطأ تجميع عادي ، عليك الانتظار لمدة ساعتين قبل الوقوع في وقت التشغيل بسبب النوع الخطأ. لكنني لا أرى مدى سوء القيام بنفس عمليات التحميل من تحت بيثون. بالمناسبة ، يحتوي OneAPI بالفعل على Intel Distribution for Python ، وهو مناسب للغاية لمختلف التقييمات.

أي أنه في عالم الأحلام لعشاق Python ، تكتب برنامجًا عليه وتفريغه إلى جميع المعجلات التي يمكنك العثور عليها باستخدام OneAPI ، وليس مجموعة من المكتبات الخاصة بالمورد. شيء آخر هو أنه من خلال هذا النهج ، تفوت الكتابة من البداية إلى النهاية وتعود إلى عالم محفوف بالمخاطر للغاية من البرمجة القائمة على واجهة برمجة التطبيقات. ربما يشجع تطوير DPC ++ المجتمع على استخدام الأدوات الأكثر ملاءمة بشكل أكثر فعالية ، مثل C ++.

صورة سؤال من البريد
الأداء مقابل OpenCL. يجب أن تكون هناك ضرائب على الرفاهية - أي التكاليف العامة. هل هناك أي قياسات؟

الجواب. على الإنترنت ، يمكنك العثور على الكثير من القياسات مع مجموعة متنوعة من النتائج ، اعتمادًا على المترجم والمهمة وجودة التنفيذ. كبحث شخصي ، قمت بقياس المهام البسيطة (SGEMM ، DGEMM) على جهاز الكمبيوتر المحمول الخاص بي (رسومات Skylake المدمجة) ، ورأيت أنه حتى الآن هناك بعض التراجع (في المئة). ولكن يبدو لي أن هذا هو نتيجة حقيقة أن كل هذا هو بيتا حتى الآن.

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

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

 deviceQueue.submit([&](cl::sycl::handler &cgh) { auto A = bufferA.template get_access<sycl_read>(cgh); auto B = bufferB.template get_access<sycl_read>(cgh); auto C = bufferC.template get_access<sycl_write>(cgh); .... deviceQueue.submit([&](cl::sycl::handler &cgh) { auto A = bufferA.template get_access<sycl_read>(cgh); auto B = bufferB.template get_access<sycl_read>(cgh); auto D = bufferD.template get_access<sycl_write>(cgh); 

هنا ، يرى المترجم أن كلتا الحزمتين فقط قرأت A و B وكتبت مخازن مؤقتة C و D ، ونتيجة لذلك ، يرى القدرة على إرسالها بالتوازي إذا كان هناك أحجام عالمية كافية.

بالطبع ، يمكن لبرنامج OpenCL المكتوب ببراعة أن يفعل ذلك أيضًا ، لكن وقت التطوير الذي يقضيه مع نواة غير تافهة لن يكون قابلاً للمقارنة.

صورة سؤال من البريد
هل جميع الطرق لتحسين تطبيقات OpenCL لـ DPC ++ مناسبة؟ ما الجديد الذي يجب إضافته إليهم؟

الجواب. أود أن أقول إن معظم التحسين اليدوي الدقيق الذي يتم تنفيذه بواسطة كتاب kernel يمكن ويجب أن يتم بواسطة المترجم. بالطريقة نفسها ، على سبيل المثال ، أعتبر أنه من الممارسات الضارة تثبيت أداة التجميع المضمنة يدويًا في برامج C ++ ، لأنه حتى لو كان يعطي فوائد تكتيكية ، فإنه يتداخل مع التحسينات ويعمل كعامل سلبي في تطوير المنتج ونقله. حسنا ، OpenCL هو الآن أيضا المجمع.

أما بالنسبة للإجابة الأكثر تفصيلاً ، فأنا أخاف من الهاوية هنا. على سبيل المثال ، يوجد مستند Intel معروف "OpenCL Developer Guide لـ Intel Processor Graphics". وهناك قسم حول كيفية المحاولة ، حتى لا نضع حيث تزامن الزائدة.

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

من ناحية أخرى ، في DPC ++ ، بدلاً من كتابة التعليمات البرمجية مع الحواجز الصريحة ، مثل هذا:

  for (t = 0; t < numTiles; t++) { const int tiledRow = TS * t + row; const int tiledCol = TS * t + col; Asub[col][row] = A[globalRow * AY + tiledCol]; Bsub[col][row] = B[tiledRow * BY + globalCol]; // Synchronise to make sure the tile is loaded barrier(CLK_LOCAL_MEM_FENCE); // .... etc .... 

من المحتمل أن تكتب تكرارًا صريحًا لـ parallel_for_work_group ، ضمن أي مجموعة. parallel_for_work_item

 cgh.parallel_for_work_group<class mxm_kernel>( cl::sycl::range<2>{BIG_AX / TS, BIG_BY / TS}, cl::sycl::range<2>{TS, TS}, [=](cl::sycl::group<2> group) { // .... etc .... for (int t = 0; t < numTiles; t++) { group.parallel_for_work_item([&](cl::sycl::h_item<2> it) { // .... etc .... Asub[col][row] = A[globalRow][tiledCol]; Bsub[col][row] = B[tiledRow][globalCol]; }); //      ,    

نتيجةً لذلك ، لست مضطرًا إلى ضبط المزامنة يدويًا على الإطلاق ، ويمكن إخراج القسم بالكامل.

وهكذا يمكنك المشي في جميع الأقسام. سوف يبقى شيء ما ، سوف يترك شيء ما. أتوقع ظهور وثيقة جديدة "Optimization for DPC ++" ، ولكن يجب أن يمر الوقت ، حيث يتم تطوير جميع تقنيات العمل الحقيقية فقط في وقت لاحق وبالدم

صورة سؤال من البريد
هناك قيود في OpenCL - لا يمكنك استخدام "بيانات بعيدة" في النواة ، على سبيل المثال ، تطبيق "مرشح واسع" يستخدم بيانات الإدخال من مجموعة كبيرة من البكسل أكبر من مجموعة عمل OpenCL في حساب واحد. ماذا يقدم DPC ++ في هذا الصدد؟

الجواب. حسنًا ، هذا مستحيل. بالطبع ، أنا لا أكتب النواة بشكل خاص ... لكن من المؤكد تمامًا أنه يمكنك استخدام جميع الذاكرة العالمية كما هي ، تحتاج فقط إلى التأكد من أنك تعمل مع العمليات الذرية (أو مزامنة النواة الهرمية من الخارج). ويمكنك أيضًا توصيل System SVM (جيدًا ، أو هناك USM في DPC ++).

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

وهكذا ، إذا تحدثنا عن حلول مباشرة وفعالة ، إذن ، بالطبع ، لا يوجد سحر في DPC ++. لا يزال البرنامج في النهاية مقسمًا إلى أجزاء: رمز المضيف ورمز الجهاز ، وجميع قيود الجهاز تؤثر على رمز الجهاز. الحد الأقصى لحجم مجموعة العمل هو أن التوازي الحقيقي أن الأجهزة الخاصة بك قادرة على. كل ما سبق هو مجرد طرق للخروج ، مما يؤثر سلبًا على الأداء. هذا هو السبب في أن DPC ++ يوفر فرصة للقيام بذلك: device.get_info <sycl :: info :: device :: max_work_group_size> () ومن ثم تحديد كيفية التعايش مع الرقم الناتج.

سيكون من المغري ، بالطبع ، صنع نموذج في DPC ++ ، عندما يعمل المبرمج كما تريد مع الحلقات من أي طول ، ويبحث المترجم في ما يجب القيام به بعد ذلك ، لكنه سيكون خاطئًا بشكل مميت ، لأنه سيخفي الثوابت ، بل حتى تقارب التعقيدات المضافة الحوسبة التي تظهر من العدم. لسبب آخر ، كتب ألكساندريسكو أن "تغليف التعقيد يجب اعتباره جريمة" ، وهذا ينطبق أيضًا.

في بعض الأحيان مراجعة الخوارزمية نفسها يساعد. هنا DPC ++ يجعل الأمور أسهل لأن كود منظم أكثر يسهل refactor. ولكن هذا هو عزاء جدا.

صورة سؤال من البريد
يستند DPC ++ إلى SYCL. ولكن ماذا لو ذهبت أعمق تحت غطاء محرك السيارة ، ما هي الاختلافات من OpenCL في تنفيذ النهاية الخلفية ، إن وجدت. على سبيل المثال ، هل آلية التوزيع بين الأجهزة غير المتجانسة هي نفس OpenCL؟

الجواب. إذا كنت تحت غطاء محرك السيارة ، فهذا هو OpenCL. جميع مزايا وقوة SYCL هي مزايا ونقاط القوة في اللغة ، أي الواجهة الأمامية. من الواجهة الأمامية ، يأتي SPIRV القديم الجيد الذي ينتقل إلى الواجهة الخلفية ويتم تحسينه (غالبًا ما يكون بالفعل في وقت التشغيل ، مثل JIT) بالفعل للحصول على بطاقة فيديو محددة بالطريقة نفسها التي يتم بها تحسين OpenCL لها.

شيء آخر هو أن آلية توزيع العمل بين الأجهزة غير المتجانسة هي مجرد واجهة أمامية أكثر من الواجهة الخلفية ، لأنها رمز المضيف الذي يحدد ما يجب إرساله وأين. ويتم الحصول على رمز المضيف من DPC ++. لقد عرضت بالفعل مثالًا أعلى قليلاً عن كيفية قيام المترجم ، بناءً على أدوات الوصول ، باتخاذ قرار بشأن حزم التوازي. وهذا هو مجرد غيض من فيض.

صورة سؤال من البريد
المكتبة. نعم ، نحن لا نتحدث عن CUDA. لكننا نعلم أنه بالنسبة لمطوري CUDA ، هناك مكتبات مفيدة للغاية تعمل بأداء عالٍ على وحدة معالجة الرسومات. يحتوي OneAPI أيضًا على بعض المكتبات ، ولكن ، على سبيل المثال ، IPP - لا يوجد أي شيء مفيد في الأرشيف للتعامل مع الصور في oneAPI / OpenCL. هل سيكون هناك شيء ما ، وكيف يمكن في هذه الحالة التبديل من CUDA إلى oneAPI؟

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

يجعل تنوع واجهات برمجة التطبيقات الحالية من استخدام عالم الإمكانيات هذا أمرًا صعبًا للمبرمجين ذوي الخبرة في وحدة المعالجة المركزية الكلاسيكية. مما يؤدي إلى OneAPI أو شيء من هذا القبيل. هنا ليس السحر في اختراق Intel في الرسومات ، ولكن في حقيقة أن Intel تفتح الباب أمام DPC ++ للجميع. نحن لا نملك حتى معيار SYCL ، بل ينتمي إلى مجموعة Khronos وجميع امتدادات Intel هي امتدادات في Khronos حيث يمكن لأي شخص الالتزام (وهناك ممثلون من جميع اللاعبين الرئيسيين هناك). وهذا يعني (المكتبات) وسيظهر المجتمع (يظهر بالفعل) ، ومجموعة من الشواغر في هذا الاتجاه.

وبالطبع ، سيتم إعادة كتابة IPP للحقائق الجديدة. لا علاقة لي بـ IPP ، لكن استخدام DPC ++ أمر منطقي ، وهناك أشخاص عاقلون يجلسون هناك.

لكن الأهم من ذلك ، هو الآن مجرد لحظة في التاريخ حيث يمكنك كتابة مكتبتك الخاصة ، والتي ستتجاوز IPP والتي سيستخدمها العالم بأسره. لأن المعايير المفتوحة دائما الفوز.

صورة سؤال من البريد
إذا قارنا إطلاق خوارزميات الشبكة العصبية للتدريب والاستدلال على Nervana و FPGA - ما هي الاختلافات في البرمجة والكفاءة الناتجة؟

الجواب. لا أعرف أي شيء عن تفاصيل البرمجة FPGA ، وأنا أكتب المترجمين. لكن لدي سؤال مضاد. وكيف سنقارن؟ وفقًا للمعايير المعيارية ، لا يشبه Nervana لعقها. ولكن في حال كان لديك شيء مثير للاهتمام ، فإن FPGA ستفك يديك ، ووضع هذا الشيء على Nervana يمكن أن يكون طويلًا ومكلفًا ، هذا كل شيء.

اتضح أن السؤال نفسه هو ، كما كان ، من المسلسل "من هو أقوى من الفيل أو الحوت". لكن هذا ليس سؤال حقيقي. السؤال الحقيقي هو: كيف يمكن استخدام الفيل والحوت في عربة واحدة؟ حسنا ، أو على الأقل توزيع ، على سبيل المثال ، لفيل لسحبه عن طريق البر ، والحوت عن طريق البحر.

في حالة OneAPI ، سيكون لديك البرنامج نفسه بشكل عام ، C ++ القياسي. ويمكنك كتابتها بنفسك وتشغيلها مع إلغاء التحميل ذهابًا وإيابًا. ستكون هذه هي المهمة التي تهمك ، والتي يمكنك من خلالها قياس الأداء وتحسينه. سيكون وجود معيار واحد وواجهة واحدة للأجهزة غير المتجانسة خطوة نحو مقارنة التفاح مع التفاح في مثل هذه الأمور.

على سبيل المثال: "ما هو الأفضل بالنسبة إلى٪ من مهمتي٪ من وجهة نظر سهولة البرمجة والكفاءة - ضع هذا الجزء على FPGA ، اترك هذا الجزء على Nervana أو قسّم هذا الجزء إلى قسمين ، وأعد كتابة هذا الجزء على GPU؟"

والقصة بأكملها مع OneAPI - إنها فقط لتقول ، "لماذا تفكر في الأمر لفترة طويلة ، سأحاول الآن بسرعة ، إنها بسيطة".

ليس بعد ، ليس سهلا. ولكن سيكون هناك.



خاتمة من الخبير

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

آمل أن أتمكن من الاهتمام بشخص ما في البرمجة غير المتجانسة و DPC ++. أريد أن أوصي الجميع sycl.tech الموقع ، حيث تكمن الكثير من التقارير ، بما في ذلك من الخبراء المشهورين عالميا (مطلوب الإنجليزية)

جيد للجميع!

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

Source: https://habr.com/ru/post/ar479486/


All Articles