سؤال

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

ويتكرر هذا عدة مرات مع وظائف الكشف المختلفة.

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

ما هي أفضل طريقة لتنظيم عمليات الكتابة للتدفقات الجديدة لتحسين النطاق الترددي للذاكرة؟ أعتقد بشكل خاص أن فهم استخدام ذاكرة التخزين المؤقت وحجم خط ذاكرة التخزين المؤقت قد يلعب دورًا كبيرًا في هذا.تخيل أسوأ حالة حيث يكون لدي 64 تدفقًا للإخراج، ولسوء الحظ، يتم تعيين العديد منها إلى نفس سطر ذاكرة التخزين المؤقت.ثم عندما أكتب الـ 64 بت التالية من البيانات إلى الدفق، يجب على وحدة المعالجة المركزية أن تقوم بطرد سطر ذاكرة التخزين المؤقت القديم إلى الذاكرة الرئيسية، وتحميل سطر ذاكرة التخزين المؤقت المناسب.يستخدم كل واحد منهم 64 بايت من النطاق الترددي ...لذلك قد يؤدي تطبيقي المحدود للنطاق الترددي إلى إهدار 95٪ من النطاق الترددي للذاكرة (في هذه الحالة الافتراضية الأسوأ).

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

أنا أستخدم معالجات Core II x86 إذا كان ذلك يحدث أي فرق.

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

2 تيارات الإخراج:13 ثانية
8 تيارات الإخراج:13 ثانية
32 تيارات الإخراج:19 ثانية
128 تيارات الإخراج:29 ثانية
512 تدفقات الإخراج:47 ثانية

الفرق بين استخدام 512 مقابل 2 من تدفقات الإخراج هو 4X، (ربما؟؟) بسبب الحمل الزائد لإخلاء خط ذاكرة التخزين المؤقت.

#include <stdio.h>
#include <stdlib.h>
#include <ctime>

int main()
{
  const int size=1<<19;
  int streambits=3;
  int streamcount=1UL<<streambits; // # of output bins
  int *instore=(int *)malloc(size*sizeof(int));
  int **outstore=(int **)malloc(streamcount*sizeof(int *));
  int **out=(int **)malloc(streamcount*sizeof(int));
  unsigned int seed=0;

  for (int j=0; j<size; j++) instore[j]=j;

  for (int i=0; i< streamcount; ++i) 
    outstore[i]=(int *)malloc(size*sizeof(int));

  int startTime=time(NULL);
  for (int k=0; k<10000; k++) {
    for (int i=0; i<streamcount; i++) out[i]=outstore[i];
    int *in=instore;

    for (int j=0; j<size/2; j++) {
      seed=seed*0x1234567+0x7162521;
      int bin=seed>>(32-streambits); // pseudorandom destination bin
      *(out[bin]++)=*(in++);
      *(out[bin]++)=*(in++);
    }

  }
  int endTime=time(NULL);
  printf("Eval time=%ld\n", endTime-startTime);
}
هل كانت مفيدة؟

المحلول

أثناء الكتابة إلى صناديق الإخراج البالغ عددها 64، ستستخدم العديد من مواقع الذاكرة المختلفة.إذا تم ملء الصناديق بشكل عشوائي، فهذا يعني أنه سيكون لديك في بعض الأحيان علبتين تشتركان في نفس سطر ذاكرة التخزين المؤقت.ليست مشكلة كبيرة.ذاكرة التخزين المؤقت Core 2 L1 هي 8 اتجاهات.هذا يعني أنك لن تواجه مشكلة إلا مع سطر ذاكرة التخزين المؤقت التاسع.مع وجود 65 مرجعًا للذاكرة الحية فقط في أي وقت (قراءة واحدة/64 كتابة)، يكون الارتباط ذو 8 اتجاهات أمرًا جيدًا.

يبدو أن ذاكرة التخزين المؤقت L2 عبارة عن 12 اتجاهًا (إجمالي 3/6 ميجابايت، لذا فإن 12 ليس رقمًا غريبًا).لذلك، حتى لو كان لديك تصادمات في L1، فمن المحتمل أنك لم تصل إلى الذاكرة الرئيسية.

ومع ذلك، إذا لم يعجبك هذا، فأعد ترتيب الصناديق في الذاكرة.بدلاً من تمشيط كل سلة بشكل تسلسلي، قم بتشذيرها.بالنسبة للحاوية 0، قم بتخزين القطع 0-15 عند الإزاحات 0-63، لكن قم بتخزين القطع 16-31 عند الإزاحة 8192-8255.بالنسبة للحاوية 1، قم بتخزين القطع 0-15 في الإزاحات 64-127، إلى آخره.لا يستغرق هذا سوى عدد قليل من التحولات والأقنعة، ولكن النتيجة هي أن زوجًا من الصناديق يتشارك في 8 أسطر من ذاكرة التخزين المؤقت.

هناك طريقة أخرى ممكنة لتسريع التعليمات البرمجية الخاصة بك في هذه الحالة وهي SSE4، خاصة في وضع x64.ستحصل على 16 سجلًا × 128 بت، ويمكنك تحسين القراءة (MOVNTDQA) للحد من تلوث ذاكرة التخزين المؤقت.لست متأكدًا مما إذا كان ذلك سيساعد كثيرًا في سرعة القراءة، على الرغم من ذلك - أتوقع أن يلتقط الجلب المسبق لـ Core2 هذا.قراءة الأعداد الصحيحة المتسلسلة هي أبسط أنواع الوصول الممكنة، ويجب على أي أداة جلب مسبق تحسين ذلك.

نصائح أخرى

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

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

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

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

إليك بعض الأفكار إذا كنت تشعر باليأس حقًا...

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

الحل الآخر الذي قد تفكر فيه هو إجراء المعالجة على بطاقة الرسومات باستخدام لغة مثل CUDA.يتم ضبط بطاقات الرسومات بحيث تتمتع بنطاق ترددي عالي جدًا للذاكرة ولإجراء عمليات حسابية سريعة على الفاصلة العائمة.توقع قضاء ما بين 5x إلى 20x وقت تطوير كود CUDA مقارنة بتطبيق C المباشر وغير المحسّن.

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

هناك أطر عمل مثل ACE (http://www.cs.wustl.edu/~schmidt/ACE.html) أو التعزيز (http://www.boost.org) يتيح لك ذلك كتابة التعليمات البرمجية التي تقوم بتعيين الذاكرة بطريقة مستقلة عن النظام الأساسي.

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

على سبيل المثال:حتى في حالة عدم وجود سحق ذاكرة التخزين المؤقت (يتم تعيين تدفقات الإخراج الخاصة بك إلى نفس خطوط ذاكرة التخزين المؤقت)، إذا كنت تكتب حجم ints، مع size = 1<<19 وsizeof(int)=4، 32 بت - أي.إذا كنت تكتب 8 ميجابايت من البيانات، فأنت في الواقع تقرأ 8 ميجابايت ثم تكتب 8 ميجابايت.لأنه إذا كانت بياناتك موجودة في ذاكرة WB (WriteBack) عادية على معالج x86، فلكي تكتب في سطر، عليك أولاً قراءة النسخة القديمة من السطر - على الرغم من أنك ستتخلص من البيانات المقروءة.

يمكنك التخلص من حركة قراءة RFO غير الضرورية هذه عن طريق (أ) استخدام ذاكرة WC (ربما يكون الإعداد صعبًا) أو (ب) استخدام مخازن تدفق SSE، المعروفة أيضًا باسم متاجر NT (غير المؤقتة).MOVNT* - MOVNTQ، MOVNTPS، إلخ.(هناك أيضًا حمل دفق MOVNTDQA، على الرغم من أنه أكثر إيلامًا عند الاستخدام.)

أنا أحب هذه الورقة التي وجدتها للتو من خلال البحث في جوجل http://blogs.fau.de/hager/2008/09/04/a-case-for-the-non-temporal-store/

الآن:تنطبق MOVNT* على ذاكرة WB ولكنها تعمل مثل ذاكرة WC، وذلك باستخدام عدد صغير من مخازن تجميع الكتابة المؤقتة.يختلف العدد الفعلي حسب طراز المعالج:كان هناك 4 فقط على شريحة Intel الأولى التي تحتوي عليها، P6 (المعروف أيضًا باسم Pentium Pro).اوووه...يوفر برنامج Bulldozer's 4K WCC (Write Combining Cache) بشكل أساسي 64 مخازن مؤقتة مدمجة للكتابة، لكل http://semiaccurate.com/forums/showthread.php?t=6145&page=40, ، على الرغم من وجود 4 مخازن مؤقتة كلاسيكية فقط للمرحاض.لكن http://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf تقول أن بعض المعالجات تحتوي على 6 مخازن مؤقتة للمرحاض، وبعضها 8.على أي حال ...هناك عدد قليل، ولكن ليس كثيرا.عادة لا 64

ولكن إليك شيئًا يمكنك تجربته:تنفيذ الكتابة الجمع بين نفسك.

أ) الكتابة إلى مجموعة واحدة مكونة من 64 مخزنًا مؤقتًا (#streams)، حجم كل منها 64B (حجم سطر ذاكرة التخزين المؤقت)، - أو ربما 128 أو 256B.دع هذه المخازن المؤقتة تكون في ذاكرة WB العادية.يمكنك الوصول إليها من خلال المتاجر العادية، على الرغم من أنه يمكنك استخدام MOVNT*، فهذا رائع.

عندما يمتلئ أحد هذه المخازن المؤقتة، قم بنسخه كتدفق إلى المكان في الذاكرة حيث من المفترض أن يذهب الدفق بالفعل.استخدام مخازن البث MOVNT*.

سينتهي هذا الأمر في القيام بذلك * n بايت مخزنة إلى المخازن المؤقتة المؤقتة ، حيث ضربت ذاكرة التخزين المؤقت L1 * 64 * 64 بايت قراءة لملء المخازن المؤقتة المؤقتة * n القراءة من المخازن المؤقتة المؤقتة ، وضربت ذاكرة التخزين المؤقت L1.* N بايت مكتوب عبر مخازن البث - ينتقل بشكل أساسي مباشرة إلى الذاكرة.

على سبيل المثال، ضرب ذاكرة التخزين المؤقت N بايت قراءة + N بايت ذاكرة التخزين المؤقت ضرب الكتابة + N بايت ذاكرة التخزين المؤقت ملكة جمال

مقابل N بايت ذاكرة التخزين المؤقت تفوت القراءة + N بايت ذاكرة التخزين المؤقت للكتابة.

قد يؤدي تقليل عدد N بايت من ذاكرة التخزين المؤقت المفقودة للقراءة إلى تعويض الحمل الإضافي.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top