C #: File.ReadLines () مقابل File.ReadAllLines () - ولماذا يجب أن أهتم؟

قبل أسبوعين ، صادفت أنا واثنان من الفرق التي أعمل معها مناقشة حول الطرق الفعالة لمعالجة الملفات النصية الكبيرة.

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

التحدي

لذلك ، فإن المشكلة قيد المناقشة هي:

  • افترض أن هناك ملف CSV كبيرًا ، قل حوالي 500 ميجابايت للمبتدئين
  • يجب أن يمر البرنامج من خلال كل سطر من الملف ، وتحليله وإجراء بعض الخرائط / تقليل الحسابات القائمة

والسؤال في هذه المرحلة من المناقشة هو:

ما هي أكثر الطرق كفاءة لكتابة الشفرة القادرة على تحقيق هذا الهدف؟ في حين الامتثال أيضا مع:
ط) تقليل مقدار الذاكرة المستخدمة و
ii) تقليل أسطر رمز البرنامج (إلى حد معقول بالطبع)

من أجل الوسيطة ، يمكننا استخدام StreamReader ، لكن قد يؤدي ذلك إلى كتابة المزيد من التعليمات البرمجية التي تحتاجها ، وفي الحقيقة ، يحتوي C # بالفعل على أساليب ملائمة File.ReadAllLines () و File.ReadLines (). لذلك يجب علينا استخدام تلك!

أرني الرمز

من أجل المثال ، دعونا نفكر في برنامج:

  1. يأخذ ملف نصي كمدخل حيث كل سطر هو عدد صحيح
  2. يحسب مجموع جميع الأرقام في الملف

من أجل هذا المثال ، سنتخطى رسائل التحقق من الصحة إلى حد ما :-)

في C # ، يمكن تحقيق ذلك عن طريق الكود التالي:

var sumOfLines = File.ReadAllLines (filePath)
    . حدد (السطر => int.Parse (سطر))
    .مجموع()

بسيط جدا ، أليس كذلك؟

ماذا يحدث عندما نطعم هذا البرنامج بملف كبير؟

إذا قمنا بتشغيل هذا البرنامج لمعالجة ملف بحجم 100 ميجابايت ، فهذا ما نحصل عليه:

  • استهلك ذاكرة الوصول العشوائي (RAM) سعة 2 جيجابايت لإكمال هذه الحوسبة
  • الكثير من GC (كل عنصر أصفر هو المدى GC)
  • 18 ثانية لإكمال التنفيذ
راجع للشغل ، تسبب تغذية ملف 500 ميجابايت لهذا الكود في تعطل البرنامج مع OutOfMemoryException Fun ، أليس كذلك؟

الآن دعونا نجرب File.ReadLines () بدلاً من ذلك

دعنا نغير الرمز لاستخدام File.ReadLines () بدلاً من File.ReadAllLines () ونرى كيف ستسير الأمور:

var sumOfLines = File.ReadLines (filePath)
    . حدد (السطر => int.Parse (سطر))
    .مجموع()

عند تشغيله ، نحصل الآن على:

  • 12 ميغابايت من ذاكرة الوصول العشوائي المستهلكة ، بدلاً من 2 جيجابايت (!!)
  • فقط 1 GC المدى
  • 10 ثوان لإكمال ، بدلا من 18

لماذا يحدث هذا؟

TL ، DR ، الاختلاف الرئيسي هو أن File.ReadAllLines () يبني سلسلة [] تحتوي على كل سطر من الملف ، وتتطلب ذاكرة كافية لتحميل الملف بأكمله ؛ على عكس File.ReadLines () الذي يغذي البرنامج كل سطر في وقت واحد ، والتي تتطلب فقط الذاكرة لتحميل سطر واحد.

في مزيد من التفاصيل:

يقوم File.ReadAllLines () بقراءة الملف بأكمله مرة واحدة وإرجاع سلسلة [] حيث يتوافق كل عنصر من الصفيف مع سطر من الملف. هذا يعني أن البرنامج يحتاج إلى ذاكرة بقدر حجم الملف لتحميل المحتويات من الملف. بالإضافة إلى الذاكرة اللازمة لتحليل جميع عناصر السلسلة إلى int ومن ثم حساب Sum ()

على الجانب الآخر ، يقوم File.ReadLines () بإنشاء عداد في الملف ، وقراءته سطراً (باستخدام StreamReader.ReadLine () بالفعل). هذا يعني أن كل سطر يتم قراءته وتحويله وإضافته إلى المبلغ الجزئي في وضع الخط المباشر.

استنتاج

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

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

بالإضافة إلى ذلك ، تتميز LINQ بالمرونة الكافية للتعامل مع هذين السيناريوهين بسلاسة وتوفير كفاءة ممتازة عند استخدامها مع رمز يوفر "تدفق" للقيم.

هذا يعني أنه لا يجب أن يكون كل شيء قائمة أو T [] مما يعني أنه تم تحميل مجموعة البيانات بأكملها على الذاكرة. باستخدام IEnumerable ، نجعل رمزنا عامًا لاستخدامه مع الطرق التي توفر مجموعة البيانات بالكامل في الذاكرة أو التي توفر قيمًا في وضع "التدفق".