مقدمة
هناك مثل هذه الأداة البسيطة والمفيدة للغاية في العالم -
BDelta ، وقد حدث ذلك حتى أصبحت جذورها في عملية الإنتاج لدينا لفترة طويلة (على الرغم من تعذر تثبيت إصدارها ، لكنها بالتأكيد ليست الأخيرة المتوفرة). نستخدمه للغرض المقصود منه - بناء بقع ثنائية. إذا نظرت إلى ما هو موجود في المستودع ، يصبح الأمر محزنًا بعض الشيء: في الواقع ، تم التخلي عنه منذ زمن طويل وكان الكثير قديمًا هناك (بمجرد أن قام زميلي السابق بعدة تصويبات هناك ، ولكنه كان منذ زمن طويل). بشكل عام ، قررت إحياء هذا العمل: لقد
تشرعت ،
وأطردت ما لم أخطط لاستخدامه ،
وتخطيت المشروع على
cmake ،
وتمشيا مع microfunctions "الساخنة" ، وإزالة صفائف كبيرة من المكدس (ومصفوفات متغيرة الطول ، والتي "قصفت" منها بصراحة) ، مرة أخرى قاد المحلل - واكتشف أن حوالي 40 ٪ من الوقت يقضيه على
الفريت ...
إذن ما الأمر مع fwrite؟
في هذا الرمز ، يُطلق على fwrite (في حالة الاختبار الخاصة بي: إنشاء تصحيح بين ملفات 300 ميجابايت ، بيانات الإدخال في الذاكرة تمامًا) ملايين المرات باستخدام مخزن مؤقت صغير. من الواضح أن هذا الشيء سيتباطأ ، وبالتالي أود التأثير بطريقة ما على هذا الخزي. لا توجد رغبة في تنفيذ أي نوع من مصادر البيانات ، غير متزامن المدخلات والمخرجات ، كنت أرغب في إيجاد حل أسهل. أول ما يتبادر إلى الذهن هو زيادة حجم المخزن المؤقت
setvbuf(file, nullptr, _IOFBF, 64* 1024)
لكنني لم أحصل على تحسن كبير في النتيجة (شكلت الآن فريت حوالي 37 ٪ من الوقت) - وهذا يعني أن الأمر لا يزال غير موجود في تسجيل البيانات المتكررة إلى القرص. إذا نظرنا إلى fwrite "أسفل الغطاء" ، يمكنك أن ترى أن بنية FILE للقفل / الفتح تحدث داخل مثل هذا (الكود الزائف ، تم إجراء جميع التحليلات في Visual Studio 2017):
size_t fwrite (const void *buffer, size_t size, size_t count, FILE *stream) { size_t retval = 0; _lock_str(stream); __try { retval = _fwrite_nolock(buffer, size, count, stream); } __finally { _unlock_str(stream); } return retval; }
وفقًا للملف التعريفي ، تمثل حسابات _fwrite_nolock 6٪ فقط من الوقت ، والباقي عبارة عن تكاليف إضافية. في حالتي الخاصة ، تعد سلامة
سلاسل الرسائل فائضًا واضحًا ،
وسأضحي بها عن طريق استبدال مكالمة fwrite بـ
_fwrite_nolock - حتى مع وجود حجج لا أحتاج لأن أكون
أذكياء . الإجمالي: أدى هذا التلاعب البسيط في بعض الأحيان إلى تقليل تكلفة تسجيل النتيجة ، والتي بلغت في الإصدار الأصلي نصف تكلفة الوقت تقريبًا. بالمناسبة ، في عالم POSIX هناك وظيفة مماثلة -
fwrite_unlocked . وبصفة عامة ، ينطبق الشيء نفسه على الخوف. وبالتالي ، بمساعدة زوج #define ، يمكنك الحصول على حل عبر الأنظمة الأساسية بدون أقفال غير ضرورية إذا لم تكن ضرورية (وهذا يحدث في كثير من الأحيان).
fwrite ، _fwrite_nolock ، setvbuf
دعنا نستخلص من المشروع الأصلي ونبدأ في اختبار حالة معينة: تسجيل ملف كبير (512 ميجابايت) في أجزاء صغيرة للغاية - 1 بايت. نظام الاختبار: AMD Ryzen 7 1700 ، 16 جيجابايت من ذاكرة الوصول العشوائي ، HDD 3.5 "7200 دورة في الدقيقة 64 ميغابايت من ذاكرة التخزين المؤقت ، نظام التشغيل Windows 10 1809 ، تم بناء binar 32 بت ، تم تضمين التحسينات ، يتم ربط المكتبة بشكل ثابت.
عينة للتجربة:
#include <chrono> #include <cstdio> #include <inttypes.h> #include <memory> #ifdef _MSC_VER #define fwrite_unlocked _fwrite_nolock #endif using namespace std::chrono; int main() { std::unique_ptr<FILE, int(*)(FILE*)> file(fopen("test.bin", "wb"), fclose); if (!file) return 1; constexpr size_t TEST_BUFFER_SIZE = 256 * 1024; if (setvbuf(file.get(), nullptr, _IOFBF, TEST_BUFFER_SIZE) != 0) return 2; auto start = steady_clock::now(); const uint8_t b = 77; constexpr size_t TEST_FILE_SIZE = 512 * 1024 * 1024; for (size_t i = 0; i < TEST_FILE_SIZE; ++i) fwrite_unlocked(&b, sizeof(b), 1, file.get()); auto end = steady_clock::now(); auto interval = duration_cast<microseconds>(end - start); printf("Time: %lld\n", interval.count()); return 0; }
ستكون المتغيرات TEST_BUFFER_SIZE ، وفي بضع حالات سنستبدل fwrite_unlocked بـ fwrite. لنبدأ بحالة fwrite دون تحديد حجم المخزن المؤقت بشكل صريح (تعليق خارج setvbuf والرمز المقترن به): الوقت 27048906 μs ، سرعة الكتابة - 18.93 ميغابايت / ثانية. الآن قم بتعيين حجم المخزن المؤقت إلى 64 كيلو بايت: الوقت - 25037111 μs ، السرعة - 20.44 ميجابايت / ثانية. الآن نقوم باختبار تشغيل _fwrite_nolock دون استدعاء setvbuf: 7262221 مللي ثانية ، تبلغ السرعة 70.5 ميجابايت / ثانية!
بعد ذلك ، قم بتجربة حجم المخزن المؤقت (setvbuf):

تم الحصول على البيانات بمتوسط 5 تجارب ؛ كنت كسولًا جدًا في التفكير في الأخطاء. بالنسبة لي ، 93 ميغا بايت / ثانية عند كتابة 1 بايت إلى HDD منتظم هو نتيجة جيدة للغاية ، ما عليك سوى اختيار حجم المخزن المؤقت الأمثل (في حالتي 256 كيلو بايت - فقط على اليمين) واستبدال fwrite بـ _fwrite_nolock / fwrite_unlocked (في إذا لم يكن هناك حاجة سلامة الموضوع ، بطبيعة الحال).
وبالمثل مع الغرابة في ظروف مماثلة. الآن لنرى كيف تسير الأمور على نظام Linux ، تكوين الاختبار على النحو التالي: AMD Ryzen 7 1700X ، 16 RAM RAM ، HDD 3.5 "7200 دورة في الدقيقة 64 ميغابايت من ذاكرة التخزين المؤقت ، OS OpenSUSE 15 ، GCC 8.3.1 ، سنختبر نظام x86-64 binar ، نظام الملفات على قسم اختبار ext4 نتيجة fwrite دون تحديد حجم المخزن المؤقت في هذا الاختبار بشكل صريح هو 67.6 ميجا بايت / ثانية ، عند ضبط المخزن المؤقت على 256 كيلو بايت ، زادت السرعة إلى 69.7 ميجا بايت / ثانية ، والآن سنقوم بإجراء قياسات مماثلة لـ fwrite_unlocked - النتائج 93.5 و 94.6 ميغابايت / ثانية ، على التوالي. أدى اختلاف حجم المخزن المؤقت من 1 كيلو بايت إلى 8 ميغابايت إلى الاستنتاجات التالية: زيادة المخزن المؤقت يزيد من سرعة الكتابة ، لكن الفرق في حالتي كان 3 ميجابايت / ثانية فقط ، ولم ألاحظ أي فرق في السرعة بين المخزن المؤقت 64 كيلوبايت و 8 ميجابايت على الإطلاق. من البيانات الواردة على جهاز Linux هذا ، يمكننا استخلاص النتائج التالية:
- fwrite_unlocked أسرع من fwrite ، لكن الفرق في سرعة الكتابة ليس بنفس حجم نظام Windows
- حجم المخزن المؤقت على Linux ليس له تأثير كبير على سرعة الكتابة من خلال fwrite / fwrite_unlocked كما هو الحال في Windows
إجمالاً ، الطريقة المقترحة فعالة على كل من Windows ، ولكن أيضًا على Linux (وإن كان ذلك بدرجة أقل بكثير).
خاتمة
كان الغرض من هذه المقالة هو وصف تقنية بسيطة وفعالة في العديد من الحالات (لم أجد الدوال _fwrite_nolock / fwrite_unlocked سابقًا ، فهي ليست شائعة جدًا - لكن دون جدوى). لا أدعي أنها مادة جديدة ، لكنني آمل أن تكون المقالة مفيدة للمجتمع.