May 28, 2014

อย่าเสียเวลาให้กับลูปที่เรียก System บ่อยๆ

ช่วงนี้มีวิเคราะห์ string บนไฟล์จำนวนมหาศาล (หลายแสนไฟล์) ตอนแรกก็เขียน Python ง่ายๆ ตามความเคยชิน
counter = 0
for file in glob('*.cpp'):
    if '//' in file.read():
        counter += 1
print(counter)
โค้ดนี้ทำงานได้ดีเลยแหละ ติดตรงที่ไฟล์ที่นำมาวิเคราะห์บางไฟล์ก็ใช้ encoding แปลกๆ แล้วเปิดอ่านไม่ได้ (ยังงงอยู่ว่า Python ไม่เช็คว่าเป็น ASCII หรือ UTF-8 ให้อัติโนมัติ?) ก็แก้ๆ โค้ดด้านบนนี้โดยใช้โมดูล codecs เข้าช่วย ไม่มีปัญหาอะไร

แต่แล้วก็คิดขึ้นมาได้ว่า งานประเภทนี้ถ้าทำใน Bash ไปเลยก็น่าจะแจ่มเหมือนกัน เพราะปรัชญาของ Unix คือให้โปรแกรมจัดการกับ I/O Stream แบบข้อความอยู่แล้ว เลยได้โค้ดออกมาเป็น
counter=0
for file in *.cpp
do
    grep -q '//' "$file" && : $((counter++))
done
echo $counter
ผลลัพท์ออกมาไม่ต่างกับ Python ด้านบน แต่ว่า Bash ทำงานช้ามากกกกก (ก.ไก่ 140 ตัว) ตอนแรกก็คิดว่า Bash มันน่าจะทำลูปไม่เก่งมั้ง เพราะเท่าที่อ่านโค้ดของโครงการ Linux ก็ไม่ค่อยเห็นเขียนลูปกันเท่าไหร่ แต่พอลองเขียนลูปเปล่าแสนรอบใน Bash มันก็ไม่เห็นจะช้า เลยคิดว่าน่าจะเป็นที่ grep มั้ง? แต่ก็ไม่อยากจะเชื่ออยู่ดีเพราะเคยอ่านงานของ Russ Cox ก็ไม่เห็นว่า grep มันจะช้าซักเท่าไหร่

อาจารย์ @juggapong ผ่านมาเห็นเลยยิงคำถามมาว่า มันช้าเพราะมัวแต่มุดเข้ามุดออก system หรือเปล่า?

ได้ยินดังนั้นเลยใช้ time จับเวลาดู โอ้ว user กับ sys ใช้เวลาพอๆ กันเลยแฮะ

ถึงตอนนี้ก็รู้แล้วว่าสาเหตุที่มันช้าเพราะ grep แต่ละครั้งเป็น process ใหม่เลย ต้องรอให้ system จองทรัพยากรเสร็จก่อนจึงจะใช้งานได้ แถมพอจบงานก็ทำลาย process นี้ทิ้งซะอีก ไม่ได้จัดสรรทรัพยากรอะไรกันระหว่างลูปเลย ... เทียบง่ายๆ คงเหมือนกับการที่มีเตารีดแสนอัน เสียบปลั๊กเตารีดตัวแรกรอร้อนแล้วจึงรีดผ้าหนึ่งชิ้น พอผ้าชิ้นนี้เรียบเนี๊ยบจนพอใจก็ถอดปลั๊กเตารีดซะงั้น แล้วไปเสียบปลั๊กเตารีดอีกตัวมารีดผ้าอีกชิ้นนั่นเอง

คิดได้ดังนี้ก็เปลี่ยนโค้ดไปเป็น (ใช้เตารีดตัวเดียว เสียบปลั๊กรอร้อนครั้งเดียว)
grep -l '//' *.cpp | wc -l
คราวนี้เร็วส์เสียยิ่งกว่า Python อีก :P

(แต่สุดท้ายก็คงกลับไปเขียน Python อยู่ดี เพราะมีปัจจัยที่ใช้วิเคราะห์ซับซ้อนกว่านี้ ใช้ Python แล้วรู้สึกถนัดมือกว่า)