Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เข้าใจ MJIT ใน Ruby 2.6: คอมไพเลอร์ Just-In-Time เพิ่มประสิทธิภาพอย่างไร

ประสิทธิภาพของ Ruby ดีขึ้นเรื่อยๆ รุ่นต่อรุ่น… ทีมพัฒนากำลังพยายามทำให้ Ruby เร็วขึ้นอีก!

หนึ่งในความพยายามคือโปรเจกต์ 3×3

เป้าหมาย?

Ruby 3.0 จะเร็วกว่า Ruby 2.0 สามเท่า

ส่วนหนึ่งของโปรเจกต์นี้คือคอมไพเลอร์ MJIT ใหม่ ซึ่งเป็นหัวข้อบทความนี้

MJIT คืออะไร

MJIT ย่อมาจาก "Method Based Just-in-Time Compiler"

หมายความว่าอะไร?

Ruby คอมไพล์โค้ดของคุณเป็น YARV instructions คำสั่งเหล่านี้รันบน Ruby Virtual Machine

JIT เพิ่มชั้นอีกชั้นหนึ่ง

มันจะ คอมไพล์คำสั่งที่ใช้บ่อย ให้กลายเป็น binary code

ผลลัพธ์คือ binary ที่ปรับให้เหมาะสม รันโค้ดของคุณเร็วขึ้น

วิธีการทำงาน

มาสำรวจว่า MJIT ทำงานยังไงเพื่อเข้าใจดีขึ้น

เปิดใช้งาน JIT ด้วย Ruby 2.6 และตัวเลือก --jit

แบบนี้:

ruby --jit app.rb

Ruby 2.6 มาพร้อมตัวเลือกเฉพาะ JIT ช่วยให้เราเห็นวิธีการทำงานได้ รัน ruby --help เพื่อดู

รายการตัวเลือก

  • --jit-wait
  • --jit-verbose
  • --jit-save-temps
  • --jit-max-cache
  • --jit-min-calls

ตัวเลือก verbose ดูเป็นจุดเริ่มต้นที่ดี!

เราจะใช้ --jit-wait ด้วย ทำให้ Ruby รอจนกว่าการคอมไพล์ JIT จะเสร็จก่อนรัน

ขณะทำงานปกติ JIT คอมไพล์โค้ดใน worker thread และไม่รอให้เสร็จ

คำสั่งทดสอบ:

ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "4.times { 123 }"

ผลลัพธ์:

Successful MJIT finish

ไม่ค่อยน่าสนใจ JIT ไม่ทำอะไรเลย

ทำไม? เพราะค่าเริ่มต้น JIT ทำงานต่อเมื่อเมธอดถูกเรียก 5 ครั้ง (jit-min-calls) ขึ้นไป

ลองรัน:

ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "5.times { 123 }"

ตอนนี้ได้อะไรน่าสนใจ:

JIT success (32.1ms): block in <main>@-e:1 -> /tmp/_ruby_mjit_p13921u0.c

ข้อความนี้บอกอะไร? JIT คอมไพล์บล็อกเพราะเราเรียก 5 ครั้ง บอกว่า:

  • ใช้เวลาคอมไพล์นานเท่าไหร่ (32.1ms)
  • คอมไพล์อะไร (block in <main>)
  • ไฟล์ที่สร้างขึ้น (/tmp/_ruby_mjit_p13921u0.c) เป็น source สำหรับการคอมไพล์นี้

ไฟล์นี้เป็น C source code คอมไพล์เป็น object file (.o) แล้วค่อยเป็น shared library (.so)

เข้าถึงไฟล์เหล่านี้ได้ด้วยตัวเลือก --jit-save-temps

ตัวอย่าง:

เข้าใจ MJIT ใน Ruby 2.6: คอมไพเลอร์ Just-In-Time เพิ่มประสิทธิภาพอย่างไร

ความเข้าใจปัจจุบันของฉันเกี่ยวกับ วิธีการทำงานของ JIT:

  1. นับการเรียกเมธอด
  2. เมื่อเมธอดถูกเรียก 5 ครั้ง (ค่าเริ่มต้น jit-min-calls) กระตุ้น JIT
  3. สร้าง C file ที่มีคำสั่งสำหรับเมธอดนี้ (เป็น YARV instructions แบบ inlined)
  4. คอมไพล์ในพื้นหลัง (เว้นแต่ --jit-wait) ด้วย C compiler ธรรมดา เช่น GCC
  5. คอมไพล์เสร็จ shared library ที่ได้จะถูกใช้เมื่อเมธอดนี้ถูกเรียก

ลองดูว่ามีประสิทธิภาพจริงไหม

ทดสอบ MJIT: เร็วขึ้นจริงไหม?

เป้าของ MJIT คือทำให้ Ruby เร็วขึ้น ตอนนี้ทำได้ดีแค่ไหน?

มาดู!

ก่อน Microbenchmarks:

Benchmark ผลลัพธ์ (เทียบกับ Ruby 2.6 ไม่มี JIT)
while เร็วขึ้น 8 เท่า
while with string append เร็วขึ้น 10%
while with multiplication (Integer) เร็วขึ้น 4 เท่า
while with multiplication (Bignum) ช้าลง 20%
string upcase เร็วขึ้น 10%
string match ช้าลง 2%
string match? เร็วขึ้น 10%
array with 10k random numbers เร็วขึ้น 20%

ดูเหมือนประสิทธิภาพกระจายอยู่ทั่วๆ ไป แต่สรุปได้ว่า…

MJIT ชอบ loops มาก!

แต่กับแอปพลิเคชันซับซ้อนมากกว่านี้ ผลลัพธ์ยังไง?

ลองกับ Sinatra app เรียบง่าย:

require 'sinatra'
get '/' do
  "apples, oranges & bananas"
end

ดูเล็กน้อย แต่โค้ดนี้รันเมธอดกว่า 500 เมธอด พอให้ JIT ทำงาน!

โดยเฉพาะ Sinatra 2.0.4 กับ Thin 1.7.2

รัน benchmark ด้วยคำสั่ง (apache bench):

ab -c 20 -t 10 https://localhost:4567/

ผลลัพธ์:

เข้าใจ MJIT ใน Ruby 2.6: คอมไพเลอร์ Just-In-Time เพิ่มประสิทธิภาพอย่างไร

เห็นได้ว่า Ruby 2.6 เร็วกว่า 2.5 แต่ เปิด JIT ทำให้ Sinatra ช้าลง 11%!

ทำไม? ไม่แน่ใจ อาจเป็น overhead ของ JIT หรือโค้ดยังไม่ได้รับการปรับให้เหมาะสม

ทดสอบด้วย C profiler (callgrind) พบว่าโค้ดที่ JIT ปรับให้เหมาะสม (C files ที่คอมไพล์แล้ว) ใช้ได้น้อยมากใน Sinatra (น้อยกว่า 2%) แต่สูงมาก (24.22%) สำหรับ while statement ที่ได้ 8x speed boost

ผลลัพธ์ while benchmark กับ JIT:

เข้าใจ MJIT ใน Ruby 2.6: คอมไพเลอร์ Just-In-Time เพิ่มประสิทธิภาพอย่างไร

ผลลัพธ์ Sinatra benchmark กับ JIT:

เข้าใจ MJIT ใน Ruby 2.6: คอมไพเลอร์ Just-In-Time เพิ่มประสิทธิภาพอย่างไร

นี่อาจเป็นส่วนหนึ่งของเหตุผล ผมไม่ใช่ compiler expert จึงไม่รู้สรุปได้

สรุป

MJIT คือ "Just-in-Time Compiler" ใน Ruby 2.6 เปิดใช้งานด้วย flag --jit MJIT มีศักยภาพและเร่งความเร็วโปรแกรมเล็กได้ แต่ยังมีงานอีกมาก!

ถ้าชอบบทความนี้別忘了แชร์ให้เพื่อนร่วมงาน Ruby 🙂

ขอบคุณที่อ่าน