1 คะแนน โดย GN⁺ 2025-03-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อนำซอร์ส jq เดิมมาบิลด์ใหม่และเปลี่ยน allocator เวลาในการประมวลผล GeoJSON ขนาด 500MB ลดจาก 4.606 วินาทีเป็น 2.428 วินาที ทำให้เร็วกว่าไบนารีของ Ubuntu 1.90 เท่า
  • การ benchmark ทำบน Ryzen 9 9950X โดยใช้ parcel map ของ Alameda County Assessor และดึง SitusCity ที่ตรงเงื่อนไข TotalNetValue < 193000
  • แค่บิลด์ใหม่ธรรมดาก็ปรับปรุงได้ 2~4% แล้ว และชุด clang-18, -O3, -flto, -DNDEBUG ให้ประสิทธิภาพ 1.20 เท่า เมื่อเทียบกับแพ็กเกจ Ubuntu
  • จาก profile พบว่าต้นทุน การจัดสรรหน่วยความจำ สูง จึงเปรียบเทียบ TCMalloc, jemalloc, mimalloc และในการทดลองด้วย LD_PRELOAD นั้น mimalloc เร็วที่สุด
  • บิลด์สุดท้ายที่ลิงก์กับ mimalloc ยังทำเวลา 0.755 วินาที เทียบกับ 1.424 วินาที ในกรณีประมวลผล JSON ขนาด 2.2GB แยกต่างหาก แสดงว่า build เริ่มต้นของดิสโทรอาจต่างกันมากตาม workload

workload อ้างอิงและวิธีวัดผล

  • สิ่งที่ทดสอบคือเครื่องมือประมวลผล JSON jq โดยข้อมูล input เป็นไฟล์ GeoJSON ขนาด 500MB ที่บรรจุ parcel map ของ Alameda County Assessor
  • query ที่รันคือการพิมพ์ SitusCity ของรายการใน parcel list ที่ตรงเงื่อนไข TotalNetValue < 193000
    • .features[] | select(.properties.TotalNetValue < 193000) | .properties.SitusCity
  • /usr/bin/jq ค่าเริ่มต้นของ Ubuntu ใช้เวลาประมาณ 5 วินาที เมื่อไฟล์ถูก cache แล้ว และ benchmark รายละเอียดวัดซ้ำด้วย hyperfine
  • เพื่อลดความแปรปรวนระหว่างรัน จึงตรึงไว้ที่ logical CPU 2 ด้วย taskset -c 2
    • เป็นการตั้งค่าเพื่อหลีกเลี่ยงผลกระทบจาก system interrupt ที่รันบน CPU 0 และ CPU migration

การบิลด์ซอร์สเดิมใหม่แบบธรรมดา

  • ดาวน์โหลด jq source code ที่ Ubuntu ใช้ แล้วรัน configure และ build โดยไม่ใส่ flag เพิ่มเติม
  • แค่บิลด์ใหม่แบบธรรมดานี้ก็เร็วกว่าแพ็กเกจไบนารีของ Ubuntu ประมาณ 2~4%
    • ไบนารีที่บิลด์ใหม่: เฉลี่ย 4.517 วินาที
    • Ubuntu /usr/bin/jq: เฉลี่ย 4.641 วินาที
    • ผลคือประสิทธิภาพประมาณ 1.03 เท่า

การใช้ clang และ flag สำหรับ optimization

  • ขั้นถัดมา ใช้ clang-18, optimization level ที่สูงขึ้น, LTO และ flag ที่เกี่ยวกับ debugging/profiling ร่วมกัน
  • flag หลักที่ส่งผลต่อประสิทธิภาพคือ -O3, -flto, -DNDEBUG
    • -O3 ใช้ระดับ optimization ที่สูงกว่า -O2
    • -flto เปิดใช้ link-time optimization
    • -DNDEBUG ลดต้นทุนของ assertion ที่เห็นเด่นใน profile
  • ตัวอย่าง configure ที่ใช้มีดังนี้
    • CC=clang-18
    • LDFLAGS="-flto -g -Wl,--emit-relocs -Wl,-z,now -Wl,--gc-sections -fuse-ld=lld"
    • CFLAGS="-flto -DNDEBUG -fno-omit-frame-pointer -gmlt -march=native -O3 -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
  • บิลด์นี้เร็วกว่าไบนารี Ubuntu 1.20 เท่า
    • บิลด์ใหม่แบบ optimize: เฉลี่ย 3.853 วินาที
    • Ubuntu /usr/bin/jq: เฉลี่ย 4.631 วินาที

การทดลองเปลี่ยน allocator

  • jq เป็นโปรแกรม C ที่ซับซ้อน และใน profile พบว่า การจัดสรรหน่วยความจำ เป็นต้นทุนที่ใหญ่ที่สุด
  • เริ่มจากการลิงก์ TCMalloc ที่มีเป็นแพ็กเกจใน Ubuntu แล้วบิลด์ใหม่
    • เพิ่ม -L/usr/lib/x86_64-linux-gnu -ltcmalloc_minimal ลงใน LDFLAGS
    • ไบนารีที่บิลด์ใหม่: เฉลี่ย 3.253 วินาที
    • Ubuntu /usr/bin/jq: เฉลี่ย 4.611 วินาที
    • ผลคือเร็วกว่าไบนารี Ubuntu 1.42 เท่า
  • แม้จะเปลี่ยนเฉพาะ allocator ด้วย LD_PRELOAD บนไบนารี Ubuntu เดิม ก็ยังปรับปรุงได้บางส่วน
    • ค่าเริ่มต้น: เฉลี่ย 4.601 วินาที
    • preload TCMalloc: เฉลี่ย 4.082 วินาที
    • เร็วกว่าค่าเริ่มต้น 1.13 เท่า

dynamic preload และการตั้งค่า THP

  • เปรียบเทียบ jemalloc, mimalloc, TCMalloc ที่ Ubuntu มีให้ด้วย LD_PRELOAD
  • การเปรียบเทียบนี้ได้ผลหลังตั้ง environment variable ต่อไปนี้
    • MIMALLOC_LARGE_OS_PAGES=1
    • MALLOC_CONF="thp:always,metadata_thp:always"
    • GLIBC_TUNABLES=glibc.malloc.hugetlb=1
  • ผลคือ mimalloc เร็วที่สุด
    • glibc ค่าเริ่มต้น: เฉลี่ย 4.123 วินาที
    • preload TCMalloc: เฉลี่ย 4.130 วินาที
    • preload jemalloc: เฉลี่ย 3.510 วินาที
    • preload mimalloc: เฉลี่ย 3.154 วินาที
  • การเปิดใช้ THP ให้ประโยชน์กับทั้ง glibc allocator, jemalloc และ mimalloc
  • THP + mimalloc เร็วกว่า THP + glibc 31% และเร็วกว่า glibc ค่าเริ่มต้น 48%

ผลลัพธ์สุดท้ายของบิลด์ที่ลิงก์ mimalloc

  • เมื่อเห็นว่า dynamic preload ไม่ใช่วิธีที่เหมาะที่สุดด้านประสิทธิภาพ ขั้นสุดท้ายจึงบิลด์ jq ใหม่โดยลิงก์กับ mimalloc
  • บิลด์สุดท้ายเร็วกว่าแพ็กเกจไบนารี Ubuntu 1.90 เท่า
    • jq ที่บิลด์ใหม่ด้วย mimalloc: เฉลี่ย 2.428 วินาที
    • Ubuntu /usr/bin/jq: เฉลี่ย 4.606 วินาที
    • benchmark แต่ละชุดอ้างอิงจากการรัน 10 ครั้ง
  • ใช้บิลด์เดียวกันนี้กับแอปพลิเคชันอื่นด้วย
    • ประมวลผล JSON ขนาด 2.2GB ในไฟล์ 13,000 ไฟล์
    • ใช้ rush สำหรับ parallelization
    • jq ที่บิลด์ใหม่ด้วย mimalloc: 0.755 วินาที
    • แพ็กเกจ Ubuntu jq: 1.424 วินาที
  • ในกรณีแยกนี้ ความเร็วที่เพิ่มขึ้นก็เกือบถึง 2 เท่า

1 ความคิดเห็น

 
GN⁺ 2025-03-19
ความคิดเห็นจาก Hacker News
  • คลิกเบตอย่าง “สร้างแพ็กเกจ Ubuntu ขึ้นใหม่หนึ่งตัวแล้วเปลี่ยน ตัวจัดสรรหน่วยความจำ ให้เร็วขึ้น 90%” นี่ชวนให้รู้สึกอยากต่อยข้าม TCP/IP ไปสักทีจริง ๆ มันเป็นแค่ แพ็กเกจเดียว เท่านั้น และการปรับปรุงบางส่วนก็ไม่ได้มาจากการคอมไพล์ใหม่ด้วยซ้ำ
    ถึงอย่างนั้น ผมเคยลองแทรก jemalloc เข้าไปในโปรแกรมหนึ่งด้วย LD_PRELOAD เพื่อเปลี่ยน implementation ของ malloc และผลลัพธ์ก็ค่อนข้างดี แม้ไม่ได้วัดประสิทธิภาพ แต่การใช้หน่วยความจำของแอปพลิเคชันนั้นเสถียรขึ้น และปัญหาที่ดูเหมือน memory leak ก็หายไปด้วย จริง ๆ แล้วมีความเป็นไปได้สูงว่าไม่ใช่ปัญหาของตัวแอปเอง แต่เป็น memory fragmentation ของ malloc มาตรฐาน

    • ผมเคยตรวจสอบตัวจัดสรรหน่วยความจำของ glibc แล้ว พบว่านี่ไม่ใช่ memory fragmentation แต่เป็นเพราะ แคชต่อเธรด ที่ไม่เคยคืนให้เคอร์เนลเลย ต่อให้เรียก free() หน่วยความจำก็แทบจะไม่ถูกปล่อยจริงจากมุมมองภายนอก ยกเว้นในกรณีพิเศษ
      ยิ่งมีเธรดและคอร์ CPU มาก ปัญหานี้ยิ่งรุนแรงขึ้น วิธีแก้ง่าย ๆ อย่างหนึ่งคือการตั้งค่าตัวแปรสภาพแวดล้อม “วิเศษ” MALLOC_ARENA_MAX=2 เพื่อจำกัดจำนวนแคช อีกวิธีคือให้แอปพลิเคชันเรียก malloc_trim() เป็นระยะเพื่อเคลียร์แคช แต่วิธีนี้ต้องแก้ซอร์สโค้ด
      https://www.joyfulbikeshedding.com/blog/2019-03-14-what-caus...
    • ใช่ ผมเองก็เกือบเชื่อไปแวบหนึ่งเหมือนกัน แต่ก็ง่ายที่จะโยนให้ Ubuntu เป็นสาเหตุของข้อผิดพลาด ส่วนตัวผมคิดว่า Ubuntu ประกอบแพ็กเกจได้ค่อนข้างดี และจริง ๆ ก็เปิดใช้ ตัวเลือกป้องกันสแตก ตอนคอมไพล์ด้วย
      กลับกัน ผมดีใจที่ไม่ได้คอมไพล์ด้วย -O3 ซึ่งอาจมีบั๊กแฝงได้ สำหรับบางส่วนที่ประสิทธิภาพสำคัญอาจจะดี แต่ผมไม่อยากให้คอมไพล์ทั้งระบบด้วย -O3
    • เห็นได้ชัดว่าเป็นการพูดเกินจริง เพราะการปรับปรุงในระดับทั้งระบบแบบนั้นไม่มีทางทำได้จากคำอธิบายในบทความเดียว เร็วขึ้น 90% เป็นตัวเลขจากไมโครเบนช์มาร์ก
    • ผมสงสัยว่าในบรรดาดิสโทรแบบไบนารีที่แพ็กเกจไว้ล่วงหน้า มีสักกี่แห่งที่บิลด์ด้วยตัวเลือกที่ปลอดภัยที่สุดสำหรับระบบปฏิบัติการและฮาร์ดแวร์ จนไม่สามารถรีดประสิทธิภาพสูงสุดที่เป็นไปได้ออกมาได้ พูดตรง ๆ คิดว่าน่าจะเป็นส่วนใหญ่
      นานมาแล้วผมเริ่มบิลด์ Mozilla และเคอร์เนล Linux ของผมเองตามความชอบ และโดยปกติก็ได้ประสิทธิภาพเพิ่มขึ้นพอสมควร จุดประสงค์ทั้งหมดของดิสโทร Gentoo Linux ก็เช่น การคอมไพล์ทุกอย่างจากซอร์สแบบปรับแต่งให้เหมาะสมเพื่อให้ได้ประสิทธิภาพที่ดีขึ้น
    • ชื่อเรื่องเป็นคลิกเบต แต่การสนับสนุนให้นักพัฒนาแอปลองบิลด์ใหม่ก็เป็นเรื่องดี โดยเฉพาะเมื่อ CPU เป็นคอขวดในยูทิลิตีที่พบได้บ่อยบางตัวอย่าง jq, grep, ffmpeg, ocrmypdf และยูทิลิตี Unix ทั่วไปแบบนี้มักถูกบิลด์ให้เป็นเป้าหมายใช้งานทั่วไป ไม่ใช่สำหรับแอปพลิเคชันเฉพาะ
  • วิศวกรรมคือการประนีประนอม ประโยชน์ส่วนใหญ่ในบทความมาจากการปรับ allocator สำหรับหน่วยความจำให้เฉพาะทาง ต้องจำไว้ว่าบางโปรเจกต์เป็นแบบหลายเธรด อาจจัดสรรในเธรดหนึ่ง เขียนข้อมูลในอีกเธรดหนึ่ง และปล่อยคืนในเธรดที่สาม
    allocator ต้องรองรับเรื่องนี้ ดังนั้นการเพิ่มความเร็วในโปรเจกต์หนึ่งอาจกลายเป็นการชนกันในอีกโปรเจกต์หนึ่งได้ กลยุทธ์การจัดสรรซ้ำก็เป็นปัญหาเช่นกัน บางโปรแกรมจัดสรรไว้ล่วงหน้าแล้วไม่แตะ malloc อีกเลย แต่บางโปรแกรมปล่อยคืนแล้วขอใหม่อยู่ตลอด ความสามารถในการจัดการ fragmentation ได้ดีแค่ไหน และ uptime เป็น 10 วินาทีหรือ 10 ปี ก็สำคัญ บางครั้งการเลือก allocator คือความแตกต่างระหว่างเสถียรภาพระยะยาวกับความเร็วระยะสั้น
    เคยทดลอง allocator หลายตัวตอนทำโปรแกรมตัดต่อวิดีโอที่ทดสอบวิดีโอ 4K และแคชเฟรม ถ้า 32MB ต่อเฟรม ที่ 60fps ก็เกือบ 2GB ต่อวินาทีต่อหนึ่งแทร็ก ชนขีดจำกัดของ allocator ทันที และตระหนักได้ว่าอย่างน้อย allocator พื้นฐานของ glibc ให้เสถียรภาพระยะยาวดีที่สุด แต่ในการ benchmark สั้นๆ กลับช้าที่สุด

    • Mimalloc เป็น allocator อเนกประสงค์ แบบเดียวกับ JEMalloc / TCMalloc glibc เป็นที่รู้กันว่าเป็น allocator ที่ค่อนข้างแย่ และ MIMalloc หรือ TCMalloc รุ่นใหม่ๆ ซึ่งไม่ใช่เวอร์ชันที่ Ubuntu ให้มาเป็นค่าเริ่มต้น นั้นนำหน้า glibc ไปมาก
      แน่นอนว่าระดับการเพิ่มความเร็วอาจแตกต่างกันได้ แต่ benchmark โดยรวมมักแสดงให้เห็นการปรับปรุงทั่วๆ ไป ส่วนมันมีความหมายกับแอปพลิเคชันเฉพาะหรือไม่นั้นเป็นอีกเรื่องหนึ่งโดยสิ้นเชิง สำหรับเรื่องการชนกัน พวกนี้ล้วนเป็น allocator อเนกประสงค์แบบหลายเธรด จึงไม่ได้ทำงานต่างจาก glibc และบั๊กก็อาจมีใน glibc ได้เหมือนกัน
    • ผมก็ทำงานกับ เฟรมวิดีโอ 8K ขนาดใหญ่เหมือนกัน [1] ถ้าหมายถึงตัวเฟรมเอง การจัดสรร 60 ครั้งต่อวินาทีนั้นไม่ใช่เรื่องใหญ่อะไรเลย เหตุผลที่ glibc ช้ามีเพียงอย่างเดียว คือการจัดสรรแต่ละครั้งเกิน DEFAULT_MMAP_THRESHOLD_MAX และบนแพลตฟอร์ม 64 บิตมันคือ 32MiB ดังนั้นจึงไม่สามารถโน้มน้าวให้ glibc แคชมันได้ตามที่เอกสารคู่มือ mallopt ระบุไว้
      ทุกครั้งจะขอหน่วยความจำจากเคอร์เนลโดยตรงด้วย mmap และคืนด้วย munmap system call เหล่านี้ช้านิดหน่อย และในกรณีของผม ค่าใช้จ่ายในการเกิด page fault ของแต่ละ memory page ตอนเข้าถึงครั้งแรก ช้าจนไม่ถึงเป้าหมายด้านประสิทธิภาพ วิธีแก้เรียบง่ายมาก คือใช้ free list ของตัวเองเฉพาะสำหรับเฟรมวิดีโอ โดยอยู่บน allocator อเนกประสงค์หรือ mmap ก็ได้ การจัดสรรขนาดเท่ากันทุกครั้งถูกทำซ้ำอย่างเสถียรมาก จึงทำงานได้ดี
      [1] ในรูปแบบ UYVY จะเล็กกว่า 64MiB เล็กน้อย และในรูปแบบ I420 จะเล็กกว่า 48MiB เล็กน้อย
    • ความเห็นนี้เข้าใจยากนิดหน่อย ผมไม่ค่อยรู้เรื่อง C หรือคอมไพเลอร์ C มากนัก แต่ได้อ่าน gist ทั้งหมดแล้วได้เรียนรู้อะไรมากมาย และรู้สึกว่ามีคุณค่า
      แต่พออ่านคอมเมนต์บนนี้แล้วก็เริ่มกังวลว่าผมอาจเข้าใจบทความผิดไปหมดหรือเปล่า จากน้ำเสียงดูเหมือนกำลังบอกว่าสิ่งที่ gist พูดถึงนั้นห้ามทำเด็ดขาด และเป็นข้อเสนอที่แย่มากซึ่งมองข้ามความซับซ้อนเหล่านี้ทั้งหมด ช่วยให้ผมเข้าใจได้ไหมว่า gist ต้นฉบับเป็นบทความที่ดีหรือไม่ มีประเด็นที่สมเหตุสมผลหรือไม่ หรือไม่มีคุณค่าเลย? ก่อนเห็นคอมเมนต์นี้ผมคิดว่ามันมีคุณค่า แต่ตอนนี้รู้ตัวแล้วว่าผมไม่ได้ฉลาดพอจะแยกแยะเอง
    • ดังนั้นการใช้ allocator ตัวเดียว กับทุกอย่างในโลกจึงเป็นความคิดที่แย่ เป็นเรื่องแย่มากที่แอปพลิเคชันเธรดเดียว หรือแม้แต่แอปพลิเคชันหลายเธรดที่จัดการทรัพยากรอย่างเข้มงวด ต้องมาจ่ายต้นทุนด้าน thread safety กันหมด
    • เป็นความเจ็บปวดที่พบได้บ่อย ถ้าใช้ภาษาที่เหมาะกับโปรเจกต์ ผู้คนก็จะบอกว่าด้วยเหตุผลนั้นเหตุผลนี้คุณควรใช้ภาษาอื่น
      ถ้าใช้อัลกอริทึมบีบอัดที่เหมาะกับข้อมูล ผู้คนก็จะบอกว่าทำไมคุณถึงโง่และควรใช้อัลกอริทึมอื่น ช่วงหลังมานี้ต้องบีบอัดสตริง JSON ยาวๆ บางแบบเพื่อใส่ลง Dynamo และเมื่อทดสอบอัลกอริทึมยอดนิยมทั้งหมดอย่างละเอียดแล้ว Brotli นำหน้าแบบขาดลอย ถึงอย่างนั้นก็ไม่ได้หยุดคนที่ผ่านมาทุกคนจากการบอกว่า zlib ดีกว่าได้ บางทีก็เหนื่อยเอาเรื่อง
  • Gentoo Linux โดยแท้จริงแล้วเป็นดิสโทรที่สร้างมาเพื่อคนกลุ่มนี้ เพื่อให้ปรับแต่งเครื่อง Linux ของตัวเองให้เหมาะกับการใช้งานของตัวเองได้
    หลังจากตั้งค่าเริ่มต้นแล้วก็ค่อนข้างเรียบง่ายและใช้งานง่าย จำได้ว่าเคยได้เพื่อนเยอะมากในช่อง Gentoo Linux บน Matrix เป็นช่วงเวลาที่สนุกดี
    https://www.gentoo.org/
    เกร็ดที่น่าสนใจคือ ChromeOS รุ่นแรก ๆ โดยพื้นฐานแล้วเป็นการติดตั้ง Gentoo Linux แบบคัสตอม ไม่รู้ว่าตอนนี้ภายในยังใช้ Gentoo Linux อยู่หรือเปล่า

    • ใช่ แต่ก็ควรชี้ไว้ว่า การปรับแต่งให้เหมาะสม ในที่นี้ไม่ได้หมายถึงประสิทธิภาพเสมอไป
      ผมใช้ Gentoo มา 20 ปี แต่ไม่เคยใช้เพราะเรื่องประสิทธิภาพเลย Gentoo ยอดเยี่ยมเมื่อคุณรู้ว่าต้องการให้มันทำงานแบบไหน และมันช่วยพาคุณไปถึงจุดนั้น
    • เพิ่งเคยเห็นมีม “install gentoo” แบบ HN แน่นอนว่าดูมีระดับขึ้น
      เป้าหมายของ Gentoo คือการมีระบบปฏิบัติการที่สร้างโปรแกรมทั้งหมดจากซอร์ส แทนที่จะใช้แพ็กเกจไบนารีที่บิลด์ไว้ล่วงหน้า สิ่งนี้ทำให้ปรับแต่งและเพิ่มความเร็วขั้นสูงได้ แต่ก็หมายความว่าแม้แต่องค์ประกอบพื้นฐานที่สุดอย่างเคอร์เนลก็ต้องคอมไพล์จากซอร์ส ในชุมชน Linux มันขึ้นชื่อว่าเป็นระบบปฏิบัติการที่ซับซ้อนมาก เพราะขั้นตอนติดตั้งที่หนักหน่วง การติดตั้ง Gentoo พื้นฐานจะบูตเข้าสู่พรอมป์ต์คำสั่งทันที และผู้ใช้ต้องแบ่งพาร์ทิชันดิสก์เอง ดาวน์โหลดและแตกแพ็กเกจที่เรียกว่า “Stage 3 tarball” ติดตั้งแพ็กเกจด้วยตนเอง และประกอบระบบขึ้นมา ผู้ใช้ใหม่หรือผู้ใช้ที่มีประสบการณ์น้อยมักไม่รู้ว่าต้องทำอะไรเมื่อเข้าโปรแกรมติดตั้งแล้วไม่มีหน้าจอกราฟิก สมาชิก /g/ มักพูดเกินจริงถึงคุณค่าของ Gentoo เพื่อหลอกให้ผู้ใช้ใหม่ลองติดตั้ง
    • ใช้ Gentoo มาตลอดตั้งแต่ปี 2003 แล้วเพิ่งย้ายตอนปลายปี 2024 ไม่นานมานี้ หลังลอง Void Linux ใน Void การที่ผู้ใช้ปลายทางสามารถบิลด์จากซอร์สได้ไม่ใช่เป้าหมายหรือฟีเจอร์เชิงสถาปัตยกรรมที่ประกาศไว้ แต่โอกาสที่จะทำให้มันใช้งานได้จริงนั้นค่อนข้างสูง
      อาจสะดุดหนึ่งสองครั้ง แต่ถ้ามีประสบการณ์ Linux โดยรวมมากพอ ก็มีแนวโน้มว่าจะเข้าไปแก้ build recipe ทำให้มันทำงานตามต้องการ และส่งแพตช์กลับ upstream ได้ นี่เป็นผลจากการโฟกัสกับความมินิมอลอย่างจริงจังและหลีกเลี่ยง overengineering ทุกรูปแบบ ซึ่งเป็นสิ่งที่ผมคิดถึงมาตลอดตอนใช้ Gentoo ใน Gentoo ผมมักลงเอยด้วยการปรับ USE flags และ package masks ในแบบที่ไม่ได้ช่วยผู้ใช้คนอื่นเท่าไร ระบบบิลด์ซับซ้อนเกินไป จนตลอดหลายปีที่ผ่านมายากเกินกว่าจะเรียนรู้ให้ชำนาญ แก้ปัญหาถึงระดับรากสาเหตุ และส่ง upstream ได้ Void อาจเป็นฐานที่เหมาะมากเช่นกันเมื่อไม่อยากบิลด์ทั้งระบบจากซอร์ส แต่อยากผสมใช้ไบนารีที่ดิสโทรให้มากับแพ็กเกจที่บิลด์เองจากซอร์ส
    • เคยใช้ Gentoo อยู่พักหนึ่ง แต่สุดท้ายมักทำระบบพังเพราะความยั่วยวนที่อยากปรับทุกอย่างไม่รู้จบ ไม่ใช่ความผิดของ Gentoo เป็นความผิดของผมเอง
      หลังจากนั้นย้ายไป ArchLinux และโดยรวมก็โอเคสำหรับผม ถ้าใช้โปรเซสเซอร์ที่ค่อนข้างมาตรฐาน ผมไม่คิดว่า Gentoo จะให้ข้อได้เปรียบมากนัก
    • เท่าที่ผมรู้ ChromeOS ที่ใช้ Gentoo เป็นฐานกำลังถูกแทนที่ด้วย Android
  • การทำแบบนี้จะทำให้ไม่เพียงพลาดอัปเดตความปลอดภัยของ jq เท่านั้น แต่ยังพลาดอัปเดตความปลอดภัยของ onigurama ซึ่งเป็น dependency สำหรับการพาร์ส regex ด้วย ก่อนหน้านี้เคยมีอัปเดตความปลอดภัยของ onigurama และถ้าเรื่องแบบนี้เกิดขึ้นอีกก็อาจมีช่องโหว่ได้ jq มักถูกใช้พาร์ส JSON ที่ไม่น่าเชื่อถือ
    เนื้อหาคือ “อัปเดตความปลอดภัย: แก้ไขการ dereference พอยน์เตอร์ที่ไม่ถูกต้องหลายรายการ, หน่วยความจำเสียหายจากการเขียนนอกขอบเขต, และ stack buffer overflow” และกรณีนั้นเกี่ยวข้องกับ CVE-2017-9224, CVE-2017-9226, CVE-2017-9227, CVE-2017-9228, CVE-2017-9229

    • ถ้าใช้ ตัวจัดการแพ็กเกจใน user space อย่าง Gentoo Prefix ก็สามารถติดตั้งบิลด์คัสตอมนี้ไปพร้อมกับยังได้รับอัปเดตความปลอดภัยต่อไปได้
    • ถึงอย่างนั้น มันก็มีไอเดียตั้งต้นของ ระบบจัดการแพ็กเกจ ที่ตัดสินใจอย่างชาญฉลาดว่าจะบิลด์หรือไม่ตามแพลตฟอร์มไม่ใช่หรือ? ประสิทธิภาพที่ทิ้งไว้ดูค่อนข้างมาก
    • โดยทั่วไปใช่ แต่กรณีนี้ไม่ถูก บิลด์ที่อธิบายใน gist ยังคง ลิงก์แบบไดนามิก กับ onigurama อยู่ onigurama อยู่ในแพ็กเกจอีกตัวชื่อ libonig5 และจะถูกอัปเดตตามปกติ
    • สงสัยว่า CVE แบบนี้โดยทั่วไปใช้ได้จริงแค่ไหน รู้สึกเหมือนบอกว่าถ้าใช้ประตูบ้านธรรมดาก็พลาดความปลอดภัยที่ประตูห้องนิรภัยให้ได้ มันก็ไม่ผิด แต่ก็มีเหตุผลที่ประตูทุกบานในธนาคารไม่ได้เป็นประตูห้องนิรภัย
      ไม่ได้ตั้งใจจะลดคุณค่าของระบบ CVE แต่ก็ปฏิเสธได้ยากว่าผลกระทบจริงระหว่างสิ่งที่พบแต่ละอย่างแตกต่างกันมาก
    • ถูกต้องแน่นอน อีกอย่าง ถ้าเปลี่ยน allocator และเปลี่ยน compiler flags ก็อาจบังเอิญมีภูมิคุ้มกันต่อการโจมตีที่พึ่งพา การจัดวางหน่วยความจำ แบบเฉพาะได้ด้วย
  • ไม่ได้ยุ่งกับเรื่องแบบนี้มาสักพักแล้ว แต่เท่าที่จำได้ ทันทีที่ใช้ flags เกินกว่าที่นักพัฒนา upstream ใช้ ก็จะเจอบั๊กแปลก ๆ และความไม่สนใจอย่างมหาศาลเมื่อเกิดบั๊กขึ้น ตรงนี้หมายถึง นักพัฒนา upstream ไม่ใช่แพ็กเกจเจอร์ของดิสโทร
    ไม่เคยใช้ malloc ที่ไม่ใช่ของ libc แต่คิดว่าหลักการเดียวกันน่าจะใช้ได้

    • มีสองสิ่งที่ตรงข้ามกันแต่เป็นจริงพร้อมกัน ถ้าคนคนหนึ่งพยายามไม่แตกต่างเลย ก็จะอยู่บนเส้นทางเดียวกับคนจำนวนมากที่สุด และในระยะสั้นมีโอกาสสำเร็จสูงที่สุด
      แต่ถ้าทุกคนทำแบบนั้น ก็จะกลายเป็น วัฒนธรรมเดี่ยว และวัฒนธรรมเดี่ยวนั้นเปราะบางและไม่ดี โค้ดจะแข็งแรงขึ้นได้บ้างก็เพราะมันถูกบิลด์ในบริบทอื่น ๆ เช่น แพลตฟอร์ม คอมไพเลอร์ ออปชัน ไลบรารี ฯลฯ ที่ต่างกัน บั๊กที่แพลตฟอร์มหรือ build flags ส่วนใหญ่บังเอิญเดินเฉียดกับดักไปโดยไม่แตะ ก็ยังเป็นบั๊กอยู่ดี และการค้นพบกับแก้มันย่อมดีกว่าสำหรับโค้ด ในฐานะปัจเจก พวกเราทุกคนได้ประโยชน์เมื่อโค้ดโดยรวมแข็งแรงขึ้นแทนที่จะเปราะบาง
    • ผมบิลด์ emacs เองมานานแล้ว และยังไม่เคยเจอบั๊กแปลก ๆ คิดว่าถ้าเลี่ยง optimization ที่ไม่ปลอดภัยก็โอเค
      แน่นอนว่าผมก็เคยคิดว่า -march=native คือการปรับปรุงหลักที่เห็น แต่บทความนี้แสดงให้เห็นว่าไม่จำเป็นต้องเป็นอย่างนั้นเสมอไป แอปพลิเคชันที่ใช้ floating point ก็น่าจะมีจุดหยาบ ๆ มากกว่า
    • ในทางกลับกัน ถ้า optimization ช่วยได้อย่างสม่ำเสมอบนหลายแพลตฟอร์ม ก็สามารถโน้มน้าวให้นักพัฒนา upstream นำไปใช้เองได้ ไม่จำเป็นต้องเป็นทุกแพลตฟอร์ม และถ้าการเพิ่มประสิทธิภาพบนสถาปัตยกรรมเดียวมากพอ ก็อาจเป็นเหตุผลให้ปรับการตั้งค่าสำหรับบิลด์นั้นได้
  • ค่อนข้างใกล้เคียงกับการนำไปบิลด์ใหม่ด้วย allocator ตัวอื่น ที่ทำเบนช์มาร์กออกมาได้ดีในเวิร์กโฟลว์เฉพาะ

    • แทบอะไรก็ดีกว่า glibc malloc ได้ทั้งนั้น การที่ดิสโทรยังใช้ glibc malloc ต่อไปแทนที่จะใช้ mimalloc หรือ jemalloc นี่แทบจะเรียกว่าละเลยหน้าที่เลย
    • glibc malloc เป็นที่รู้กันไหมว่าดีกับโหลดงานแบบไหน?
  • อยากรู้ว่าประสิทธิภาพเมื่อเทียบกับโคลน jq ที่เขียนด้วย Rust ตัวนี้เป็นอย่างไร
    cargo install --locked jaq
    ถ้าต้องการเปิดการปรับแต่งให้เหมาะกับตระกูล CPU เฉพาะ ก็เพิ่ม RUSTFLAGS="-C target-cpu=native" ได้ด้วย cargo install เป็นฟีเจอร์ที่ถูกประเมินค่าต่ำของ Rust ซึ่งเหมาะพอดีกับการใช้งานแบบที่บทความอธิบายไว้ เพราะมันบิลด์เครื่องมือจากซอร์ส จึงเลือกใช้ฟีเจอร์หรือคำสั่งเฉพาะแพลตฟอร์มที่ปกติไม่ได้ใส่ไว้ในไบนารีเพื่อรักษาความเข้ากันได้กับ CPU รุ่นเก่าได้ ไม่ต้องโคลนรีโพหรือหาวิธีบิลด์ด้วยซ้ำ สิ่งนั้นมาพร้อมกันอยู่แล้ว
    jaq[1] และ yq[2] เป็นตัวเลือกที่ผมหยิบมาใช้ทุกครั้งที่ใช้ jq แล้วต้องการเพิ่มประสิทธิภาพแบบเร็วและง่าย
    [1] https://github.com/01mf02/jaq
    [2] https://github.com/mikefarah/yq

    • ผมลองเทียบ jaq กับ jq, gojq เป็นครั้งคราว โดยทดสอบด้วยคำตอบ jq ของผมสำหรับ AoC 2022 day 13
      https://gist.github.com/oguz-ismail/8d0957dfeecc4f816ffee79d...
      ตอนนี้ก็ยังตามหลังทั้งสองตัวอยู่
    • โบนัสที่หลายคนอาจไม่รู้คือ เมื่ออยากใช้รีโพโดยตรง cargo install ยังมี แฟล็ก --git สำหรับระบุ URL ของรีโพได้ด้วย ใช้ได้ตอนที่ไม่มีแพ็กเกจสาธารณะ หรืออยากได้คอมมิตล่าสุดที่ยังไม่ได้รีลีส
      ผมเคยใช้หลายครั้งมาก่อน โดยเฉพาะเป็นวิธีง่าย ๆ ในการติดตั้งเครื่องมือที่ทำขึ้นลวก ๆ ใช้ส่วนตัวอย่างรวดเร็ว หลังจาก push ขึ้นรีโพแล้ว โดยไม่ต้องทำกระบวนการรีลีส หรือคัดลอกไบนารีไปยังเครื่องส่วนตัวหลายเครื่องเอง และไม่ต้องตามเก็บว่าคอมมิตไหนกันแน่ที่ใช้บิลด์
  • ถ้าจะทำแบบนี้จริง ๆ ก็ให้ Ubuntu ดาวน์โหลด แพ็กเกจซอร์ส ที่ต้องการตรง ๆ ได้เลย ในกรณีนี้ใช้ apt-get source jq ก็พอ
    จากนั้นเข้าไปในแพ็กเกจแล้วคอมไพล์ใหม่ได้ตามใจ จะรีแพ็กเกจเพื่อแจกจ่ายหรือเก็บรักษาไว้ก็ได้ วิธีนี้จะได้ผลลัพธ์ที่ใกล้เคียงกับ Ubuntu upstream มากกว่า แทนที่จะได้ข้อผิดพลาดแปลก ๆ และความไม่สอดคล้องกันเพียบ

  • ชื่อเรื่องทำให้เข้าใจผิด หมายถึงเวลาที่เร็วขึ้น 90% และจริง ๆ แล้วคือ เร็วขึ้นประมาณ 45%
    ถ้าสนใจว่าเราใช้ภาษากันอย่างไร เรื่องนี้ก็น่าสนใจอยู่เล็กน้อย อาจพูดได้ว่าในเวลาเท่ากันทำงานได้มากขึ้น 90% ซึ่งสอดคล้องกับหน่วยวัดความเร็วแบบอื่นที่เราใช้กันทั่วไป เช่น ไมล์ต่อชั่วโมง คำต่อนาที บิตต่อวินาที แต่ในประสิทธิภาพคอมพิวเตอร์มีธรรมเนียมการวัดเวลาที่ใช้กับปริมาณงานคงที่ น่าจะเพราะโดยมากปริมาณงานคงที่ และสิ่งที่เปลี่ยนคือเวลาที่ต้องรอ สำหรับบล็อกโพสต์นี้ก็เป็นแบบนั้นพอดี จึงทำให้เอาเวลาไปไว้เป็นตัวเศษ ตัวบทความเองน่าสนใจและเขียนดีมาก แต่ไม่ได้เร็วขึ้น 90%

    • ที่ทำให้เข้าใจผิดยิ่งกว่าคือนัยว่าทำให้ทุกแพ็กเกจ เร็วขึ้น 90% ได้ นี่เป็นแพ็กเกจเฉพาะตัวเดียว
    • ในที่นี้ดูเหมือนว่าการใช้ “90% faster” ในฐานะ หน่วย throughput จะสมเหตุสมผลกว่า ไม่ใช่หน่วยเวลา
      อีกอย่าง ถ้าจะใช้หน่วยเวลา ก็คงไม่ใช้คำว่า “faster” อยู่แล้ว “45% less time” กับ “45% faster” เป็นข้ออ้างที่ต่างกันมาก และทั้งสองแบบมีความหมายทั้งในและนอกวงการโปรแกรมมิง
    • บทความที่เกี่ยวข้อง: https://randomascii.wordpress.com/2018/02/04/what-we-talk-ab...
    • คิดต่อแล้วน่าจะรู้แล้วว่าทำไมถึงทำให้เข้าใจผิด เพราะกำลังพูดถึงการเปลี่ยนแปลงของค่าที่ใหญ่กว่าเป็นเปอร์เซ็นต์ของค่าที่เล็กกว่า
      เวลาเราพูดว่า “ลดอะไรบางอย่างลง N%” โดยปกติจะถือว่า N% นั้นเป็น N% ของสิ่งที่ถูกลดลงเอง ไม่ใช่ของค่าอื่น
    • น่าจะถูกแล้ว อาจพูดได้ว่าแพ็กเกจ หรือก็คือโค้ด เร็วขึ้น 45% หรือพูดว่าเพิ่ม throughput การพาร์สขึ้น 90% แต่ถ้าเอาสองอย่างมาปนกันก็ทำให้สับสน
  • พออ่านว่าแค่เปลี่ยนง่าย ๆ แบบนี้ก็ได้ความเร็วเพิ่มขึ้นมาก สิ่งแรกที่นึกถึงคือควรบอก ผู้เขียน jq อาจมีหลุมพรางที่ต้องระวัง หรือหลังจากทดสอบแล้วอาจทำให้ทุกคนเร็วขึ้นได้
    ไม่ว่าผลจะเป็นอย่างไร การแจ้งสั้น ๆ ก็ดูมีประโยชน์ แต่ในบทความดูเหมือนไม่ได้พิจารณาตัวเลือกนั้นเลย และในคอมเมนต์นี้ก็ยังไม่เห็น ผมพลาดอะไรไปหรือเปล่า?

    • อยากรู้ว่า Intel Clear Linux ได้ประโยชน์แบบคล้ายกันจากการใช้ opcode รุ่นใหม่กว่าของชุดคำสั่งหรือไม่
      ไม่รู้เหมือนกันว่าที่นั่นใช้ glibc allocator เป็นมาตรฐานหรือเปล่า
      https://en.m.wikipedia.org/wiki/Clear_Linux_OS