2 คะแนน โดย GN⁺ 2024-10-20 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

-.NET 9.0 ลดเวลาในการรันลงอย่างมากเมื่อเทียบกับ .NET 8 ในหลาย สถานการณ์การใช้งาน LINQ ทั่วไป และในบางเบนช์มาร์กยังตัดการจัดสรรหน่วยความจำออกไปได้ด้วย

  • หนึ่งในการปรับปรุงสำคัญคือ เมื่อวนลูปกับอาร์เรย์หรือ List<T> จะใช้ TryGetSpan() เพื่อรับ ReadOnlySpan<T> และลดต้นทุนของการวนซ้ำ
  • TryGetSpan() ระบุ TSource[] และ List<TSource> ด้วยการเปรียบเทียบชนิด แต่การจับ span จากอาร์เรย์ภายในของ List<T> เป็น การปรับแต่งสาย Unsafe ที่อาจใช้งานไม่ได้หากมีการเปลี่ยนความจุ
  • LINQ ใน .NET 9 รู้จำ call chain แบบพบบ่อย เพื่อสร้าง iterator แบบเฉพาะทาง และใช้การปรับแต่งเพิ่มเติมกับเมธอดปลายทางอย่าง Count(), First(), Last(), ElementAt(), Sum()
  • เพียงแค่มายเกรตและคอมไพล์ใหม่ ก็สามารถได้ประโยชน์จากการปรับปรุงประสิทธิภาพ LINQ บางส่วน พร้อมทั้งมีการปรับแต่งอย่างการใช้ SIMD และการตรวจจับลำดับว่างตั้งแต่เนิ่นๆ รวมอยู่ด้วย

เหตุผลที่การวนอาร์เรย์และลิสต์เร็วขึ้น

  • เบนช์มาร์กแรกเก็บ Enumerable.Range(1, 10_000).ToArray() ไว้เป็น IEnumerable<int> แล้วรัน Count, All, Any, First, Single, Last เพื่อเปรียบเทียบ .NET 8 กับ .NET 9
  • ใช้ BenchmarkDotNet และโปรเจ็กต์ต้องกำหนดเป้าหมายเป็น net8.0;net9.0 พร้อมคอมไพล์ในโหมด Release
  • ใน .NET 9 เวลาในการรันของหลายเมธอดลดลงอย่างมาก และการจัดสรรหน่วยความจำก็หายไป
    • LinqCount: จาก 16,198.490 ns เป็น 3,043.563 ns, จากจัดสรร 32 B เป็นไม่จัดสรร
    • LinqAny: จาก 17,096.735 ns เป็น 2,483.927 ns, จากจัดสรร 32 B เป็นไม่จัดสรร
    • LinqFirst: จาก 15,289.747 ns เป็น 2,243.341 ns, จากจัดสรร 32 B เป็นไม่จัดสรร
    • LinqSingle: จาก 21,684.114 ns เป็น 4,884.329 ns, จากจัดสรร 32 B เป็นไม่จัดสรร
    • LinqAll: จาก 10.588 ns เป็น 2.562 ns, จากจัดสรร 32 B เป็นไม่จัดสรร
    • LinqLast: จาก 15.967 ns เป็น 6.918 ns

ความต่างที่ TryGetSpan() สร้างขึ้น

  • สาเหตุหลักของประสิทธิภาพที่ดีขึ้นคือการใช้ TryGetSpan()
  • หาก enumerable ที่วนซ้ำเป็นอาร์เรย์หรือลิสต์ TryGetSpan() จะคืนค่า ReadOnlySpan<T> เพื่อให้วนลูปได้เร็วขึ้น
  • โค้ดสาขาหลักจะตรวจ source.GetType() == typeof(TSource[]) หรือ source.GetType() == typeof(List<TSource>) แล้วจึงดึง span ออกมา
    • อาร์เรย์จะจัดการด้วย Unsafe.As<TSource[]>(source)
    • ลิสต์จะใช้ CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source)) เพื่อดึง span จากอาร์เรย์ภายใน
  • ในโค้ดมีการเรียก source.GetType() สองครั้ง และไม่ได้ใช้การตรวจ null หลัง cast แต่เป็นแนวทางที่ผู้เชี่ยวชาญด้านประสิทธิภาพของ .NET เลือกโดยคำนึงถึงการปรับแต่งของคอมไพเลอร์ C# และ JIT
  • ในสแตก .NET ที่ถูกปรับแต่งอย่างหนัก micro-optimization อาจให้ผลไม่เหมือนที่เห็นจากภายนอก

ข้อจำกัดของ CollectionsMarshal.AsSpan()

  • List<TSource> ภายในอ้างอิงอาร์เรย์อยู่ และเมื่อความจุของลิสต์ต้องเพิ่มหรือลด จะสร้างอาร์เรย์ใหม่ขึ้นมาแล้วอ้างอิงไปยังอาร์เรย์นั้น
  • CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source)) จะรับ Span<TSource> จาก อาร์เรย์ภายใน นี้
  • หากความจุของลิสต์เปลี่ยนไปไม่ว่าด้วยวิธีใด อาร์เรย์ที่ได้มาด้วยวิธีนี้อาจใช้งานต่อไม่ได้
  • ด้วยข้อจำกัดนี้ งาน Enumerable บางอย่างที่มีการวนซ้ำแบบหน่วงเวลา เช่น yield จึงพึ่งพาการปรับแต่งนี้ได้ยาก
  • ชื่อ System.Runtime.CompilerServices.Unsafe เองก็สะท้อนความเสี่ยงนี้อยู่แล้ว

ขอบเขตการเรียก TryGetSpan()

  • ใช้ NDepend สแกน System.Linq.dll เพื่อตรวจดูผู้เรียก TryGetSpan() ทั้งทางตรงและทางอ้อม
  • พาธแอสเซมบลีที่ใช้วิเคราะห์คือ C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0-rc.1.24431.7\System.Linq.dll
  • สร้าง code query จาก TryGetSpan() เพื่อระบุผู้เรียก และส่งออกเมธอดที่ตรงกัน 56 รายการไปยัง dependency graph
  • เมธอด Enumerable มาตรฐานจำนวนมากจะพยายามใช้ การวนผ่าน span เมื่อคอลเลกชันเป็นอาร์เรย์หรือลิสต์
  • แต่เนื่องจากแนวทางการจับอาร์เรย์ภายในของลิสต์ไม่ปลอดภัย จึงยังมีข้อจำกัดกับโอเปอเรชันที่ต้องการการทำงานแบบ lazy execution

การปรับแต่งบนพื้นฐานของ iterator แบบเฉพาะทาง

  • เบนช์มาร์กที่สองใช้ตัวอย่างจาก PR Consolidate LINQ’s internal IIListProvider/IPartition into base Iterator class
  • สิ่งที่ทดสอบได้แก่ Distinct().First(), Append().Select().Last(), Reverse().Count(), DefaultIfEmpty().Select().ElementAt(), Skip().Take().ElementAt(), Union().First(), Select().Where().Select().Sum() เป็นต้น
  • ใน .NET 9 call chain บางแบบเร็วขึ้นอย่างมาก
    • DistinctFirst: จาก 65.318 ns เป็น 11.192 ns, จากจัดสรร 328 B เป็นไม่จัดสรร
    • AppendSelectLast: จาก 4,122.007 ns เป็น 2.661 ns, จากจัดสรร 144 B เป็นไม่จัดสรร
    • DefaultIfEmptySelectElementAt: จาก 4,090.818 ns เป็น 5.724 ns, จากจัดสรร 144 B เป็นไม่จัดสรร
    • RangeUnionFirst: จาก 66.309 ns เป็น 6.193 ns, จากจัดสรร 344 B เป็นไม่จัดสรร
    • ListSkipTakeElementAt: จาก 6.268 ns เป็น 2.916 ns
    • RangeReverseCount: จาก 11.024 ns เป็น 6.134 ns
  • ในทางกลับกัน SelectWhereSelectSum ช้าลงจาก 3,959.622 ns ใน .NET 8 เป็น 4,460.008 ns ใน .NET 9 และยังคงมีการจัดสรร 112 B

การรู้จำ LINQ chain ที่พบบ่อย

  • ทีมประสิทธิภาพของ .NET ออกแบบโค้ดให้รู้จำ LINQ call chain ที่พบบ่อย
  • เมื่อพบ chain บางแบบ จะสร้าง iterator เฉพาะทางที่จัดการลำดับงานได้มีประสิทธิภาพกว่า
  • หาก chain ลงท้ายด้วยเมธอดอย่าง Count(), First(), Last(), ElementAt(), Sum() ก็จะสามารถปรับแต่งเพิ่มเติมได้
  • ตัวอย่างเช่น OrderBy(criteria).First() อาจถูกปรับให้ทำงานคล้าย Min(criteria)

โครงสร้าง Iterator<T> และคลาสที่สืบทอด

  • ภายใน LINQ มี abstract base class Iterator<T> และคลาสที่สืบทอดอีก 40 คลาส
  • คลาสเหล่านี้ทั้งหมดซ้อนอยู่ภายในคลาส Enumerable
  • แม้ Iterator<T> จะเป็น abstract class แต่เมธอดเป็นแบบ virtual ดังนั้นคลาสที่สืบทอดจึง override เฉพาะเมธอดที่จำเป็น
  • โครงสร้างนี้เป็นพื้นฐานสำหรับบรรจุพฤติกรรมเฉพาะตามแต่ละ call chain

กรณีของ ListWhereSelectIterator<TSource, TResult>

  • ListWhereSelectIterator<TSource, TResult> จัดการ chain Where(...).Select(...) บนลิสต์ด้วย iterator เพียงตัวเดียว
  • iterator นี้ถูกสร้างจาก Select() override ของ ListWhereIterator<TSource, TResult>
  • ListWhereIterator<TSource> ถูกสร้างเมื่อ Enumerable.Where() ตรวจพบว่า source เป็น List<TSource>
  • ListWhereSelectIterator<TSource, TResult> ไม่ได้ override เมธอดอย่าง TryGetFirst() หรือ TryGetLast()
  • หัวใจของการปรับปรุงประสิทธิภาพคือการรวม chain Where(...).Select(...) ที่พบได้บ่อยมากบนลิสต์ จาก iterator สองตัวให้เหลือ iterator ตัวเดียว
    • ภายใน MoveNext() จะเรียก delegate สองตัวคือ _predicate และ _selector ร่วมกัน

กรณีของ IListSkipTakeIterator<TSource>

  • IListSkipTakeIterator<TSource> เป็น iterator แบบเฉพาะทางที่ถูกสร้างเมื่อเข้าเงื่อนไข
  • MoveNext() ใช้ _state - 1 เป็นดัชนีแบบ zero-based ของลิสต์
  • แม้การมีฟิลด์ดัชนีแยกจะอ่านง่ายกว่า แต่เลือกเก็บแบบมี bias ไว้ใน _state เพื่อลดขนาดของฟิลด์ใน iterator
  • การปรับแต่งของ iterator นี้อยู่ที่การไม่วนผ่านสมาชิกที่อยู่นอกช่วง _minIndexInclusive และ _maxIndexInclusive โดยไม่จำเป็น

การปรับแต่งเพิ่มเติมที่ได้จากการมายเกรตอย่างเดียว

  • ใน .NET 9 หลายสถานการณ์การใช้งาน LINQ ทั่วไปทำงานได้เร็วขึ้น
  • สิ่งที่ต้องทำเพื่อรับการปรับปรุงจาก .NET เวอร์ชันใหม่คือมายเกรตและคอมไพล์ใหม่
  • LINQ ยังถูกปรับแต่งในรูปแบบอื่นด้วย
    • กรณีอย่างการรวมผลลำดับจำนวนเต็ม จะใช้ SIMD ในจุดที่ทำได้
    • ลำดับว่างจะถูกตรวจพบตั้งแต่เนิ่นๆ เพื่อลดต้นทุนของการ enumerate
  • DeepDotnet videos เป็นสื่อการเรียนรู้ .NET ที่มี Scott Hanselman และ Stephen Toub ปรากฏตัว

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

 
GN⁺ 2024-10-20
ความคิดเห็นจาก Hacker News
  • ผมมองว่าส่วนที่มีประโยชน์ที่สุดของ LINQ ไม่ใช่ โครงสร้างส่วนขยายที่อิงกับ syntax tree ของ IQueryable และไม่ใช่ไวยากรณ์ที่ฝังมากับภาษา แต่เป็น extension methods ของ IEnumerable
    เมื่อก่อนมันถูกเรียกแบบค่อนข้างชวนสับสนว่า “LINQ to Objects” และทำให้เขียน C# ในสไตล์ functional ได้อย่างกระชับ
    บทความต้นฉบับพูดถึงการปรับแต่งประสิทธิภาพของ extension methods เหล่านี้เป็นหลัก
    หลังจากเรียน Haskell แล้วผมถึงเริ่มเข้าใจแนวทางนี้ดีขึ้น และมันก็มีทั้งข้อดีและกับดักบางอย่างร่วมกับ Haskell เช่น lazy evaluation
    ถ้าใช้แบบไม่ยั้งคิดก็อาจได้โค้ดที่อ่านยากและช้า ดังนั้นถ้าในทีมไม่มีคนที่รู้ idiom พื้นฐานของ functional programming และ lazy evaluation ผมก็ไม่อยากแนะนำ

    • ผมเองก็ชอบแง่มุมแบบ functional ของส่วนขยาย LINQ สำหรับ IEnumerable และ IQueryable
      มันให้เหตุผลตามได้ง่ายกว่า และในที่อย่าง Entity Framework ก็อาจไม่ใช่ตัวเลือกที่เร็วที่สุดเสมอไป แต่โดยรวมก็เป็นตัวเลือกที่ค่อนข้างดี
      ผมชอบใช้ Dapper มากกว่า EF ด้วย
      แต่โปรเจกต์ C# มักมีแนวโน้มจะมีชั้น abstraction มากเกินเหตุ และงานพัฒนาแบบ “enterprise” โดยรวมก็มักดูแล้วทรมานใจ
    • ผมก็ใช้ LINQ แบบนี้เหมือนกัน
      แม้ชื่อบางส่วนจะไม่ค่อยเป็นมาตรฐานนัก แต่ก็มีทุกอย่างที่จำเป็น
      Eric Lippert เคยเขียนบทความชุดยอดเยี่ยมที่อธิบาย monad โดยเชื่อมกับ LINQ: https://ericlippert.com/2013/04/02/monads-part-twelve/
    • ผมใช้ LINQ แบบ method syntax มาโดยตลอด
      เพราะไม่ชอบให้มีภาษา “ฝังในตัว” อีกภาษาหนึ่งอยู่ในภาษา host และสุดท้ายผลลัพธ์ก็ต้องกลับมาเป็น C# อยู่ดี
      แม้จะไม่ได้ใช้ ORM อย่าง Entity Framework หรือ Dapper ผมก็คิดว่าควรวาง logic การเข้าถึงข้อมูล รวมถึง SQL ไว้ในโปรเจกต์ abstract แยกต่างหาก
      เพื่อไม่ให้มันกระจายไปทั่วทั้งแอปพลิเคชัน และจะได้สลับเปลี่ยนได้ถ้าวันหนึ่งต้องใช้ RDBMS ตัวอื่น
      ถึงตลอด 20 ปีที่ผ่านมาจะเกิดเรื่องแบบนั้นจริง ๆ แค่ครั้งเดียวก็ตาม
      เวลานักพัฒนาจูเนียร์ใช้ LINQ การให้ใช้ profiler และ debugger จะช่วยให้เข้าใจว่าข้างในเกิดอะไรขึ้น
      บางครั้งการให้เขียนด้วยลูป for และ logic C# ทั่วไปก่อน แล้วค่อยเทียบกับการเขียนด้วย LINQ ก็มีประโยชน์ เพื่อให้เห็นข้อดีข้อเสียของทั้งสองแนวทาง
    • ถ้าคุณชอบ Haskell คุณอาจชอบการใช้งาน LINQ แบบอื่น ๆ เช่น การประกอบ combinator parser ด้วย query syntax
      query syntax ไม่ได้ถูก hardcode มาเพื่อ IEnumerable โดยเฉพาะ เพียงแต่ค่าเริ่มต้นทำงานแบบนั้น และจริง ๆ ใช้ได้แทบทุกที่
      มันทำงานคล้ายกับ operator overloading อยู่เล็กน้อย
      [1]: https://github.com/acple/ParsecSharp/blob/da8d0cb9ec39e28dd9...
    • ถ้าไวยากรณ์ LINQ หายไปพรุ่งนี้ ผมก็คงไม่เสียดายเท่าไร แต่ functional composition นั้นทรงพลังจริง ๆ และบำรุงรักษาได้ง่ายกว่าด้วย
  • ผมไม่เข้าใจว่าทำไมทีม dotnet ถึงไม่ทุ่มทรัพยากรและเวลาให้กับเครื่องมือมากกว่านี้
    เราต้องการ doctest และการสร้างเอกสาร, ความสามารถในการเขียน unit test ที่ดีขึ้นและเร็วขึ้นไว้ข้าง ๆ โค้ดจริง, การเข้าถึงซอร์สโค้ด, สภาพแวดล้อมที่ไม่ต้อง decompile DLL เมื่อกด F12, และฮับกลางสำหรับแพ็กเกจและเอกสารแบบ pkg.go.dev หรือ docs.rs
    แพ็กเกจ NuGet ส่วนใหญ่ไม่มีเอกสารเลย หรือมีแค่ GitHub README หรือวิกิสั้น ๆ เท่านั้น
    ecosystem อื่น ๆ อย่าง Rust, Go, Java, Python นำหน้าไปหลายปีแสง ในเรื่องนี้

    • ผมเผลอแซวว่าเหตุผลที่ Microsoft ลงทุนใน OpenAI ก็เพราะมันเป็นวิธีเดียวที่พอสมเหตุสมผลในการค้นดูเอกสารแพ็กเกจ .NET/NuGet
      แต่ที่น่ากลัวคือผมก็รู้สึกว่ามันใกล้ความจริงอยู่เหมือนกัน
    • ตอนนี้เอกสารของ Microsoft มีลิงก์ไปยังซอร์สของเมธอดที่กำลังดูอยู่โดยตรงแล้ว
      ตัวอย่าง: https://learn.microsoft.com/en-us/dotnet/api/system.string.s...
      ซอร์สของแพ็กเกจ NuGet ก็ทำได้ง่ายถ้าเปิด Source Link แต่ฟีเจอร์นี้ยังค่อนข้างใหม่ จึงยังไม่ได้ถูกใช้ในทุกแพ็กเกจ
    • เห็นด้วย แต่สาเหตุหนึ่งอาจเป็นเพราะ C# แบบโอเพนซอร์สยังเป็นเรื่องค่อนข้างใหม่
      ผมเดาว่าโค้ด C# ส่วนใหญ่ยังคงถูกเขียนเป็น closed source ในองค์กร
      ถ้า Microsoft ยังเดินหน้าสู่ความเปิดกว้างเหมือนช่วงไม่กี่ปีที่ผ่านมา ก็น่าจะดีขึ้นเมื่อเวลาผ่านไป
      ฟีเจอร์บางอย่างในกลุ่มนี้มีเครื่องมืออย่าง Resharper ให้ใช้อยู่แล้ว เลยสงสัยว่ามีข้อตกลงแบบชัดแจ้งหรือโดยนัยกันหรือเปล่าว่าจะไม่ล้ำพื้นที่กัน
      พูดตามตรง เอกสารส่วนใหญ่ที่ผมเห็นในโปรเจกต์ C# มีคุณภาพต่ำ จนสุดท้ายก็ต้องไปอ่านซอร์สโค้ดอยู่ดี
      จากประสบการณ์ แม้จะมีเครื่องมือ autocomplete มากมาย แต่ก็ไม่ได้ช่วยเรื่องการอ่านเท่าไร ช่วยเฉพาะตอนเขียนมากกว่า
    • Sandcastle Help File Builder มีมานานมากแล้ว และเท่าที่จำได้เริ่มจากโปรเจกต์ภายในของ Microsoft แต่แปลกที่มีไลบรารีใช้งานมันน้อย
      https://github.com/EWSoftware/SHFB
    • มีวิธีเขียนเทสต์ไว้ข้าง ๆ โค้ดด้วย: https://clipperhouse.com/go-test-csharp/
      แต่ไม่แน่ใจว่าจะแนะนำไหม
      ผมเคยลองเองแล้วก็ย้อนกลับ เพราะดูเหมือนว่าเทสต์ใช้เวลานานขึ้น อาจเพราะการแคช build artifacts แย่ลง
  • ควรเรียกว่า “การปรับปรุงประสิทธิภาพการใช้งาน List ของตัวเอง” มากกว่า “การปรับปรุงประสิทธิภาพ LINQ”
    Microsoft ดูเหมือนจะใช้เวลาไปกับการปรับปรุงส่วนที่ตัวเองต้องการ มากกว่าการปรับปรุงทั่วไป
    LINQ โดยเฉพาะ ไวยากรณ์แบบ query ไม่ใช่แค่ extension method ยังต้องการการลงทุนเพิ่มเติม
    สิ่งที่ต้องการหลัก ๆ คือการลดการจัดสรร lambda และถ้าเป็นไปได้คือการย่อ lambda ตั้งแต่ตอนคอมไพล์
    ต้องมีกลยุทธ์อย่าง local lambda แบบ value type หรือทำให้การจัดสรร lambda ไม่กลายเป็น overhead เหมือนตอนนี้
    ตัวแปร LINQ ก็ควรรองรับ wildcard (_) ได้แล้ว แต่ตอนที่นำมาใช้กับ lambda กลับถูกมองข้ามไปโดยสิ้นเชิง
    นอกจากนี้ ในฐานะรายการสุดท้ายของนิพจน์ LINQ ควรใช้ชนิดที่ถูก lift อย่าง IEnumerable, Option แทน select ... ได้
    ในบาง use case select สร้าง overhead ที่ไม่จำเป็น และยังจำกัดสิ่งอย่างนิพจน์ LINQ แบบ tail recursion ด้วย
    ไลบรารีที่ทุ่มให้ LINQ เต็มที่แต่ไม่ใช้ IEnumerable, IQueryable หรือ LINQ extension เหมือนไลบรารีของผม ยังคงถูกมองข้ามต่อไป
    เพราะ Microsoft มุ่งแต่ปรับปรุงประสิทธิภาพให้โปรเจกต์ของตัวเอง
    ตัวอย่างที่ดีคือการปรับปรุง type inference ของ lambda
    มันถูกเร่งขึ้นมาเพราะ ASP.NET Core Minimal API ต้องการ
    ฟีเจอร์ของภาษาและเฟรมเวิร์กจำนวนมากดูเหมือนจะขับเคลื่อนด้วยความต้องการภายในของ Microsoft มากกว่าความต้องการของชุมชน
    แย่ที่สุดคือชุด เมธอดเวทมนตร์ ไม่ได้เพิ่มขึ้นแค่ LINQ extension อย่าง Select, SelectMany, Where แต่รวมถึงอย่าง GetAwaiter ด้วย
    แทนที่ Microsoft จะใส่คุณสมบัติ higher-kinded ที่จำเป็นจริง ๆ เพื่อคลี่คลายเวทมนตร์นี้ กลับเพิ่มฟีเจอร์เพื่อตัวเองเป็นหลัก โดยเฉพาะเพื่อคอมไพเลอร์
    ดังนั้นทุกอย่างจึงยังคงมีชนิดแบบอ่อน และคอมไพเลอร์ค้นพบได้เพียงคร่าว ๆ
    LINQ เป็นหนึ่งในจุดแตกต่างหลักระหว่างภาษา แต่แทบถูกปล่อยทิ้งไว้ตั้งแต่ C# 3
    น่าเสียดายจริง ๆ ที่ยังมอง LINQ ว่ามีประโยชน์แค่กับการวนลูปรายการ โดยเฉพาะการวนลูป implementation ของรายการของตัวเอง
    การปรับปรุงประสิทธิภาพเองนั้นน่าขอบคุณและจะช่วยผู้ใช้จำนวนมาก แต่โฟกัสมักแคบเกินไปจนจำกัดศักยภาพ
    [1] https://github.com/louthy/language-ext/

    • ถ้ามีฟีดแบ็กที่เป็นประโยชน์ ก็ควรเปิด issue หรือส่ง PR ไปที่ dotnet/runtime
      การปรับปรุงประสิทธิภาพ LINQ หลายอย่างที่พูดถึงในบทความก็เข้ามาด้วยวิธีนั้น
    • ไลบรารีดูน่าสนใจมาก แต่ก็มีบางส่วนที่เหมือนตั้งค่าไว้ให้ถูกมองข้ามได้ง่ายตั้งแต่แรก
      มีคำสั่ง using เยอะ แต่สำหรับคนที่เข้าใจการแบ่งโปรเจกต์ให้เล็กตามหน่วยที่จำเป็นและแยก concern ออกจากกัน นั่นไม่ใช่ปัญหาใหญ่
      แต่ developer ส่วนใหญ่ไม่ได้จัดโครงสร้างโปรเจกต์แบบนั้น และรายละเอียดเล็ก ๆ แบบนี้อาจกลายเป็นกำแพงสำหรับ developer ทั่วไปได้
      developer ระดับ junior มักลำบากกับไวยากรณ์และเมธอด LINQ มาตรฐานอยู่แล้ว โดยเฉพาะเรื่อง ประสิทธิภาพ
      ดีที่ README พูดถึงเรื่องนี้
      ปกติคนมักยุ่งกับการ “ขาย” ไลบรารี แต่ผมชอบที่เขียนไว้จริง ๆ ว่าแข็งตรงไหนและมีเป้าหมายอะไร
      การบอกว่าไม่เป็นไปตาม idiom ก็อาจเป็นปัญหาสำหรับคนที่กำลังเรียน C#/.NET
      Microsoft น่าจะอยากให้เครื่องมือและภาษาทำตามแนวปฏิบัติบางอย่าง และการตั้งชื่อที่เข้ากับ functional programming อย่างเป็นธรรมชาติอาจถูกมองว่าเป็นอุปสรรคค่อนข้างใหญ่เมื่อ Microsoft พิจารณาการปรับปรุง
      ผมกดดาว repository แล้ว และสนใจสิ่งที่ทำไว้มาก
      ในแอปพลิเคชันใหญ่ ๆ บางตัวที่เพิ่งทำเมื่อไม่นานนี้ ผมใช้ type Result ที่ดูคล้ายกับ Option คร่าว ๆ
      แต่พอกลับมาดูไลบรารีอีกครั้ง ก็รู้สึกว่าตัวเองคิดว่ารู้ functional programming พอสมควร แต่จริง ๆ แล้วยังไม่ใช่
      ผมทำ C# ได้ค่อนข้างดีและเคยทำงานซับซ้อนมาแล้ว แต่แม้อ่านเรื่อง functional programming มาเยอะ ก็ยังเรียนรู้ได้ยาก และ F# For Fun And Profit เป็นแหล่งที่เข้าใจได้ดีที่สุด
      สุดท้ายไม่ได้หมายความว่าคุณทำอะไรผิด
      Microsoft คงตั้งเป้าไปที่ developer ระดับเฉลี่ยหรือเริ่มต้น ซึ่งเป็นคนส่วนใหญ่ใน ecosystem ของตัวเอง
      หวังว่าไลบรารีนี้จะได้ประโยชน์จากการปรับปรุงภายในบ้าง
      เห็นได้ชัดว่าใช้เวลาลงแรงมหาศาล และแค่ดูจำนวนดาวใน GitHub ก็เป็นสัญญาณเพียงพอว่ามีคนใช้งานจริงและได้รับประโยชน์
      ถ้าฟังดูเหมือนคำพูดที่เมินเฉยก็ขอโทษด้วย แต่งานที่ทำไว้น่าสนใจ และเอกสารก็ดูพร้อมพอให้ค่อย ๆ เรียนรู้แนวคิดที่ยังไม่รู้จักได้
  • ยิ่ง C# ยืมสิ่งต่าง ๆ จาก F# มากเท่าไรก็ยิ่งดี
    กำลังรอให้ discriminated union เข้ามาใน C# ในที่สุด เพื่อจะทำ domain modeling ได้อย่างถูกต้อง

    • น่าสนใจที่ได้ยินเรื่องแบบนี้บ่อยในชุมชน .NET
      ทุกครั้งก็มีคำถามผุดขึ้นมาว่า “ทำไมไม่ใช้ F# ไปเลยล่ะ?”
      C# พยายามไล่ตามมาหลายปีแล้ว
      ถ้า F# เป็นตัวผลักดันนวัตกรรมจำนวนมากใน ecosystem .NET และนำหน้าหลายปีในด้านฟีเจอร์ ก็สงสัยว่าทำไมถึงไม่ตอบแทนความพยายามนั้นด้วยการใช้งานจริง
      ถ้ามีทิศทางการพัฒนาภาษาที่ต้องการ ก็ควรส่งเสริมด้วยการเลือกใช้จริง
      ใน ecosystem Java วิธีนั้นเคยได้ผล และตอนนี้ Java ก็ปรับปรุงขึ้นแล้ว
      เมื่อ market ใหญ่ขึ้น ก็อาจเกิด positive feedback loop ที่ทำให้ความพยายามด้าน engineering เพิ่มขึ้นตาม
      พออ่านฟอรัมมาหลายปี จะรู้สึกแรงมากว่าฝั่ง C# อยาก “แค่” อยู่ในฝั่งตัวเองแล้วรอ
      ดูคล้าย tribalism อยู่บ้าง เหมือนทีมของตัวเองคือ “C#”
      ใน ecosystem ภาษาอื่นไม่ค่อยเห็นวัฒนธรรมแบบนี้ และรู้สึกว่าถ้า F# อยู่ใน ecosystem อื่นที่ไม่ใช่ .NET มันอาจรุ่งเรืองไปนานแล้วก็ได้
    • ผมก็อยากได้ type สำหรับหน่วยวัด มากเช่นกัน
      การดูแลโค้ดด้านวิศวกรรมหรือวิทยาศาสตร์จะง่ายขึ้นมาก
    • ใช้ OneOf[0] กับ Dunet[1] ก็สามารถนำ discriminated union มาใช้ได้ค่อนข้างง่ายอยู่แล้ว
      ตัวอย่างการใช้งานจริง: https://chrlschn.dev/blog/2024/07/csharp-discriminated-union...
      [0] https://github.com/mcintyre321/OneOf
      [1] https://github.com/domn1995/dunet
    • เป็นความลับที่รู้กันทั่วไปว่า F# เป็น สนามทดลอง สำหรับฟีเจอร์ของ C# และ VB.NET และบุคคลทางการอย่าง Hanselman ก็เคยอ้างถึงเรื่องนี้หลายครั้ง
  • สิ่งที่คิดถึงที่สุดเวลาทำงานกับภาษาอื่นหรือ ecosystem อื่นคือ LINQ
    การที่มีฟีเจอร์แบบนี้อยู่ในไลบรารีมาตรฐานนั้นดีมากจริง ๆ และถูกออกแบบมาอย่างงดงามภายใต้ข้อจำกัดที่มี

  • ในบทความประจำปีขนาดเท่าหนังสือหนึ่งเล่มที่พูดถึงการปรับปรุงประสิทธิภาพทั้งหมดของ .NET 9 มีส่วนที่เกี่ยวข้องอยู่
    https://devblogs.microsoft.com/dotnet/performance-improvemen...
    แปลกที่ HN ไม่ยอมให้ส่งซ้ำ บทความเลยไม่ได้ขึ้นหน้าแรกและถูกกลบไป

  • พอคุ้นกับ LINQ แล้ว และมักได้ทำงานในโดเมนที่ LINQ โดดเด่น ก็จะไม่อยากกลับไปใช้วิธีอื่นอีก

    • เพื่อน ๆ อย่าเสพติด LINQ
      LINQ จะยึดคุณไว้ และทำให้คุณโทษสภาพแวดล้อมที่ไม่มี LINQ
    • ถึงอย่างนั้นก็ยังทรงพลังน้อยกว่าอย่าง polars
  • อยากรู้ว่ามีหนังสือหรือบทเรียนแบบครอบคลุมดี ๆ สำหรับเรียน การพัฒนาเว็บแบบ end-to-end ด้วย dotnet ไหม
    ที่เคยหาเจอส่วนใหญ่พื้นฐานเกินไป หรือเก่าไป หรือคุณภาพต่ำ

    • สิ่งใหม่และมาแรงในสายพัฒนาเว็บ .NET ช่วงนี้คือ Blazor แต่ดูเหมือนจะไม่ค่อยนิยมเท่าไรนอกวงบล็อกของ Microsoft และน่าจะไม่ดังขึ้นด้วย
      ส่วนตัวมองว่าน่าจะเดินรอยเดียวกับ Silverlight
      เทคโนโลยีเก่า ๆ ก็ยังมีอยู่ใน .NET 9 ยังทำงานได้และได้รับการดูแลอยู่
      ทุกวันนี้การทำเว็บด้วย .NET โดยมากคือการสร้าง HTTP/JSON/REST API แล้วเชื่อมกับเฟรมเวิร์กฝั่งฟรอนต์เอนด์ที่ต้องการ
      กรณีของผมใช้ React หรือ NextJS
      คำค้นที่ดีคือ ASP.NET WebApi หรือถ้าเอาแบบสมัยใหม่กว่าคือ ASP.NET Minimal API
      ยังสามารถทำ server-side rendering ด้วย .NET MVC โดยใช้ Razor ได้
      เพราะเป็นภาษา markup ของ ASP.NET MVC ให้ค้นว่า “ASP.NET MVC Razor”
    • ช่วงหลังเริ่มสนใจการพัฒนาเว็บด้วย C#
      ไม่ว่าจะดีหรือแย่ วิธีสร้างเว็บแอปพลิเคชันบน .NET ดูเหมือนว่า ASP.NET จะเป็นคำตอบโดยพฤตินัย
      การที่ทางเลือกมีน้อยก็น่าสงสัยอยู่บ้าง
      ได้ฟังพอดแคสต์ที่มี Andrew Lock ผู้เขียน “ASP.NET Core in Action” ไปออก เขาดูเป็นคนที่รู้เรื่องนี้ดี
      ยังไม่ได้อ่านหนังสือ แต่ก็อาจเป็นเล่มที่กำลังหาอยู่
      1: https://dotnetcore.show/season-6/navigating-the-aspnet-core-...
      2: https://www.manning.com/books/asp-net-core-in-action-third-e...
    • อาจจะค่อนข้างนอกกระแส แต่ชุด F# กับ Fable ทรงพลังมาก
      ฝั่งเซิร์ฟเวอร์สามารถรัน Giraffe บน ASP.NET ได้ เป็นชั้น functional programming ที่ให้ประสิทธิภาพใกล้เคียง C#
      ฝั่งฟรอนต์เอนด์สามารถเขียน React ด้วยภาษา functional programming จริง ๆ ได้
      แน่นอนว่ายังแชร์โค้ด F# ระหว่างฟรอนต์เอนด์กับแบ็กเอนด์ได้ด้วย
    • ผมเรียนจากการลงมือทำเอง แต่จะจดแหล่งข้อมูลที่น่าอ้างอิงไว้
      หนังสือมี “C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals - Eighth Edition: Start building websites and services with ASP.NET Core 8, Blazor, and EF Core 8” ของ Mark J Price และ “Web API Development with ASP.NET Core 8: Learn techniques, patterns, and tools for building high-performance, robust, and scalable web APIs” ของ Xiaodi Yan
      บทเรียนมีซีรีส์ของ IAmTimCorey และ Shawn Wildermuth บน YouTube
    • ถ้าเป็น UI แบบ server rendering ให้หาแหล่งข้อมูลที่ใช้ Razor และช่วงแรกควรหลีกเลี่ยงเนื้อหา Blazor
      ถ้าเป็นชุด .NET backend กับ JS frontend ให้หาแหล่งข้อมูลที่ใช้ Minimal API
      MVC ก็ดี แต่มีภาระเรื่อง backward compatibility เยอะ จึงเกิด Minimal API ขึ้นมา
  • มันต้องมีวิธีที่ดีกว่ากองสปาเกตตี annotation แบบนี้สิ
    ทุกครั้งที่เห็นโค้ด .NET สมัยใหม่แล้วปวดตา

    • attribute เหล่านั้นสอดคล้องกับ ไลบรารี benchmark ที่ใช้ในบทความ
      โค้ด unit test กับ benchmark โดยปกติก็มักจะดูเป็นสปาเกตตีในระดับหนึ่งอยู่แล้ว
      ถึงอย่างนั้น ถ้าเป็น PR ใน business logic จริง ๆ ที่เขียนแบบนั้น ผมคงไม่อนุมัติ
      ถ้าเกลียดจริง ๆ แม้แต่อะไรอย่าง AspNetCore ก็ใช้ได้โดยไม่ต้องแตะ attribute เลยแม้แต่ตัวเดียว
    • ไม่รู้ว่าคุณกำลังดูโค้ด .NET แบบไหนอยู่
      ผมแทบไม่ใช้ attribute เลย
  • ส่วนที่บอกว่าสามารถเพิ่ม optimization ได้มากขึ้นเมื่อ chain จบด้วยเมธอดอย่าง Count(), First(), Last(), ElementAt(), Sum() และเช่น OrderBy(criteria).First() สามารถ optimize ให้ทำงานเหมือน Min(criteria) ได้นั้น อาจมีประโยชน์
    แต่ตั้งแต่แรกก็ควรเขียนโค้ดที่ดีกว่านี้อยู่แล้ว
    มันอาจน่าสนใจสำหรับ chain ที่สร้างแบบ dynamic แต่ถ้าเป็นโค้ดที่เขียนเองแล้วทำ operation แบบนี้ ก็ให้ความรู้สึกเหมือนการเสริมแรงเชิงบวกแบบบิดเบี้ยวเล็กน้อย
    คือไลบรารีรู้จัก pattern ที่ไม่มีประสิทธิภาพแล้วแก้ให้
    อย่างน้อยก็หวังว่าจะมี feedback ที่เสนอให้ปรับปรุงโค้ดพื้นฐาน