การปรับปรุงประสิทธิภาพของ LINQ ใน .NET 9.0
(blog.ndepend.com)-.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 nsRangeReverseCount: จาก 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>จัดการ chainWhere(...).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 ความคิดเห็น
ความคิดเห็นจาก 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 ผมก็ไม่อยากแนะนำ
IEnumerableและIQueryableมันให้เหตุผลตามได้ง่ายกว่า และในที่อย่าง Entity Framework ก็อาจไม่ใช่ตัวเลือกที่เร็วที่สุดเสมอไป แต่โดยรวมก็เป็นตัวเลือกที่ค่อนข้างดี
ผมชอบใช้ Dapper มากกว่า EF ด้วย
แต่โปรเจกต์ C# มักมีแนวโน้มจะมีชั้น abstraction มากเกินเหตุ และงานพัฒนาแบบ “enterprise” โดยรวมก็มักดูแล้วทรมานใจ
แม้ชื่อบางส่วนจะไม่ค่อยเป็นมาตรฐานนัก แต่ก็มีทุกอย่างที่จำเป็น
Eric Lippert เคยเขียนบทความชุดยอดเยี่ยมที่อธิบาย monad โดยเชื่อมกับ LINQ: https://ericlippert.com/2013/04/02/monads-part-twelve/
เพราะไม่ชอบให้มีภาษา “ฝังในตัว” อีกภาษาหนึ่งอยู่ในภาษา host และสุดท้ายผลลัพธ์ก็ต้องกลับมาเป็น C# อยู่ดี
แม้จะไม่ได้ใช้ ORM อย่าง Entity Framework หรือ Dapper ผมก็คิดว่าควรวาง logic การเข้าถึงข้อมูล รวมถึง SQL ไว้ในโปรเจกต์ abstract แยกต่างหาก
เพื่อไม่ให้มันกระจายไปทั่วทั้งแอปพลิเคชัน และจะได้สลับเปลี่ยนได้ถ้าวันหนึ่งต้องใช้ RDBMS ตัวอื่น
ถึงตลอด 20 ปีที่ผ่านมาจะเกิดเรื่องแบบนั้นจริง ๆ แค่ครั้งเดียวก็ตาม
เวลานักพัฒนาจูเนียร์ใช้ LINQ การให้ใช้ profiler และ debugger จะช่วยให้เข้าใจว่าข้างในเกิดอะไรขึ้น
บางครั้งการให้เขียนด้วยลูป
forและ logic C# ทั่วไปก่อน แล้วค่อยเทียบกับการเขียนด้วย LINQ ก็มีประโยชน์ เพื่อให้เห็นข้อดีข้อเสียของทั้งสองแนวทางquery syntax ไม่ได้ถูก hardcode มาเพื่อ
IEnumerableโดยเฉพาะ เพียงแต่ค่าเริ่มต้นทำงานแบบนั้น และจริง ๆ ใช้ได้แทบทุกที่มันทำงานคล้ายกับ operator overloading อยู่เล็กน้อย
[1]: https://github.com/acple/ParsecSharp/blob/da8d0cb9ec39e28dd9...
ผมไม่เข้าใจว่าทำไมทีม dotnet ถึงไม่ทุ่มทรัพยากรและเวลาให้กับเครื่องมือมากกว่านี้
เราต้องการ doctest และการสร้างเอกสาร, ความสามารถในการเขียน unit test ที่ดีขึ้นและเร็วขึ้นไว้ข้าง ๆ โค้ดจริง, การเข้าถึงซอร์สโค้ด, สภาพแวดล้อมที่ไม่ต้อง decompile DLL เมื่อกด F12, และฮับกลางสำหรับแพ็กเกจและเอกสารแบบ
pkg.go.devหรือdocs.rsแพ็กเกจ NuGet ส่วนใหญ่ไม่มีเอกสารเลย หรือมีแค่ GitHub README หรือวิกิสั้น ๆ เท่านั้น
ecosystem อื่น ๆ อย่าง Rust, Go, Java, Python นำหน้าไปหลายปีแสง ในเรื่องนี้
แต่ที่น่ากลัวคือผมก็รู้สึกว่ามันใกล้ความจริงอยู่เหมือนกัน
ตัวอย่าง: https://learn.microsoft.com/en-us/dotnet/api/system.string.s...
ซอร์สของแพ็กเกจ NuGet ก็ทำได้ง่ายถ้าเปิด Source Link แต่ฟีเจอร์นี้ยังค่อนข้างใหม่ จึงยังไม่ได้ถูกใช้ในทุกแพ็กเกจ
ผมเดาว่าโค้ด C# ส่วนใหญ่ยังคงถูกเขียนเป็น closed source ในองค์กร
ถ้า Microsoft ยังเดินหน้าสู่ความเปิดกว้างเหมือนช่วงไม่กี่ปีที่ผ่านมา ก็น่าจะดีขึ้นเมื่อเวลาผ่านไป
ฟีเจอร์บางอย่างในกลุ่มนี้มีเครื่องมืออย่าง Resharper ให้ใช้อยู่แล้ว เลยสงสัยว่ามีข้อตกลงแบบชัดแจ้งหรือโดยนัยกันหรือเปล่าว่าจะไม่ล้ำพื้นที่กัน
พูดตามตรง เอกสารส่วนใหญ่ที่ผมเห็นในโปรเจกต์ C# มีคุณภาพต่ำ จนสุดท้ายก็ต้องไปอ่านซอร์สโค้ดอยู่ดี
จากประสบการณ์ แม้จะมีเครื่องมือ autocomplete มากมาย แต่ก็ไม่ได้ช่วยเรื่องการอ่านเท่าไร ช่วยเฉพาะตอนเขียนมากกว่า
https://github.com/EWSoftware/SHFB
แต่ไม่แน่ใจว่าจะแนะนำไหม
ผมเคยลองเองแล้วก็ย้อนกลับ เพราะดูเหมือนว่าเทสต์ใช้เวลานานขึ้น อาจเพราะการแคช 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/
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 ได้อย่างถูกต้อง
ทุกครั้งก็มีคำถามผุดขึ้นมาว่า “ทำไมไม่ใช้ F# ไปเลยล่ะ?”
C# พยายามไล่ตามมาหลายปีแล้ว
ถ้า F# เป็นตัวผลักดันนวัตกรรมจำนวนมากใน ecosystem .NET และนำหน้าหลายปีในด้านฟีเจอร์ ก็สงสัยว่าทำไมถึงไม่ตอบแทนความพยายามนั้นด้วยการใช้งานจริง
ถ้ามีทิศทางการพัฒนาภาษาที่ต้องการ ก็ควรส่งเสริมด้วยการเลือกใช้จริง
ใน ecosystem Java วิธีนั้นเคยได้ผล และตอนนี้ Java ก็ปรับปรุงขึ้นแล้ว
เมื่อ market ใหญ่ขึ้น ก็อาจเกิด positive feedback loop ที่ทำให้ความพยายามด้าน engineering เพิ่มขึ้นตาม
พออ่านฟอรัมมาหลายปี จะรู้สึกแรงมากว่าฝั่ง C# อยาก “แค่” อยู่ในฝั่งตัวเองแล้วรอ
ดูคล้าย tribalism อยู่บ้าง เหมือนทีมของตัวเองคือ “C#”
ใน ecosystem ภาษาอื่นไม่ค่อยเห็นวัฒนธรรมแบบนี้ และรู้สึกว่าถ้า F# อยู่ใน ecosystem อื่นที่ไม่ใช่ .NET มันอาจรุ่งเรืองไปนานแล้วก็ได้
การดูแลโค้ดด้านวิศวกรรมหรือวิทยาศาสตร์จะง่ายขึ้นมาก
ตัวอย่างการใช้งานจริง: https://chrlschn.dev/blog/2024/07/csharp-discriminated-union...
[0] https://github.com/mcintyre321/OneOf
[1] https://github.com/domn1995/dunet
สิ่งที่คิดถึงที่สุดเวลาทำงานกับภาษาอื่นหรือ ecosystem อื่นคือ LINQ
การที่มีฟีเจอร์แบบนี้อยู่ในไลบรารีมาตรฐานนั้นดีมากจริง ๆ และถูกออกแบบมาอย่างงดงามภายใต้ข้อจำกัดที่มี
ในบทความประจำปีขนาดเท่าหนังสือหนึ่งเล่มที่พูดถึงการปรับปรุงประสิทธิภาพทั้งหมดของ .NET 9 มีส่วนที่เกี่ยวข้องอยู่
https://devblogs.microsoft.com/dotnet/performance-improvemen...
แปลกที่ HN ไม่ยอมให้ส่งซ้ำ บทความเลยไม่ได้ขึ้นหน้าแรกและถูกกลบไป
พอคุ้นกับ LINQ แล้ว และมักได้ทำงานในโดเมนที่ LINQ โดดเด่น ก็จะไม่อยากกลับไปใช้วิธีอื่นอีก
LINQ จะยึดคุณไว้ และทำให้คุณโทษสภาพแวดล้อมที่ไม่มี LINQ
อยากรู้ว่ามีหนังสือหรือบทเรียนแบบครอบคลุมดี ๆ สำหรับเรียน การพัฒนาเว็บแบบ end-to-end ด้วย dotnet ไหม
ที่เคยหาเจอส่วนใหญ่พื้นฐานเกินไป หรือเก่าไป หรือคุณภาพต่ำ
ส่วนตัวมองว่าน่าจะเดินรอยเดียวกับ 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”
ไม่ว่าจะดีหรือแย่ วิธีสร้างเว็บแอปพลิเคชันบน .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...
ฝั่งเซิร์ฟเวอร์สามารถรัน 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
ถ้าเป็นชุด .NET backend กับ JS frontend ให้หาแหล่งข้อมูลที่ใช้ Minimal API
MVC ก็ดี แต่มีภาระเรื่อง backward compatibility เยอะ จึงเกิด Minimal API ขึ้นมา
มันต้องมีวิธีที่ดีกว่ากองสปาเกตตี annotation แบบนี้สิ
ทุกครั้งที่เห็นโค้ด .NET สมัยใหม่แล้วปวดตา
โค้ด unit test กับ benchmark โดยปกติก็มักจะดูเป็นสปาเกตตีในระดับหนึ่งอยู่แล้ว
ถึงอย่างนั้น ถ้าเป็น PR ใน business logic จริง ๆ ที่เขียนแบบนั้น ผมคงไม่อนุมัติ
ถ้าเกลียดจริง ๆ แม้แต่อะไรอย่าง AspNetCore ก็ใช้ได้โดยไม่ต้องแตะ attribute เลยแม้แต่ตัวเดียว
ผมแทบไม่ใช้ attribute เลย
ส่วนที่บอกว่าสามารถเพิ่ม optimization ได้มากขึ้นเมื่อ chain จบด้วยเมธอดอย่าง
Count(),First(),Last(),ElementAt(),Sum()และเช่นOrderBy(criteria).First()สามารถ optimize ให้ทำงานเหมือนMin(criteria)ได้นั้น อาจมีประโยชน์แต่ตั้งแต่แรกก็ควรเขียนโค้ดที่ดีกว่านี้อยู่แล้ว
มันอาจน่าสนใจสำหรับ chain ที่สร้างแบบ dynamic แต่ถ้าเป็นโค้ดที่เขียนเองแล้วทำ operation แบบนี้ ก็ให้ความรู้สึกเหมือนการเสริมแรงเชิงบวกแบบบิดเบี้ยวเล็กน้อย
คือไลบรารีรู้จัก pattern ที่ไม่มีประสิทธิภาพแล้วแก้ให้
อย่างน้อยก็หวังว่าจะมี feedback ที่เสนอให้ปรับปรุงโค้ดพื้นฐาน