اشتراکها
- با easy hook میشه JIT Compiler دات نت رو هوک کرد. در اینجا فرصت خواهید داشت تا بدنه متد رو با کدهای IL بازنویسی کنید. در مورد JIT Hooking اگر اطلاعات بیشتری خواستید، اینجا
- CLR Injection: Runtime Method Replacer
- .NET CLR Injection: Modify IL Code during Run-time
- Hawkeye - The .Net Runtime Object Editor
- کار خود مایکروسافت است: Moles - Replace any .NET method with a delegate البته مایکروسافت برای کارهای Native ویندوز هم یک کتابخانه به نام detours دارد. نسخه 32 بیتی آن عمومی است و با C سازگار است.
- Modifying IL at runtime
- WPF Snoop
- CLR Injection: Runtime Method Replacer
- .NET CLR Injection: Modify IL Code during Run-time
- Hawkeye - The .Net Runtime Object Editor
- کار خود مایکروسافت است: Moles - Replace any .NET method with a delegate البته مایکروسافت برای کارهای Native ویندوز هم یک کتابخانه به نام detours دارد. نسخه 32 بیتی آن عمومی است و با C سازگار است.
- Modifying IL at runtime
- WPF Snoop
Source Generators که به همراه C# 9.0 ارائه شدند، یک فناوری نوین meta-programming است و به عنوان جزئی از پروسهی استاندارد کامپایل برنامه، ظاهر میشود. هدف اصلی از ارائهی Source Generators، تولید کدهای تکراری مورد استفادهی در برنامهها است. برای مثال بجای انجام کارهای تکراری مانند پیاده سازی متدهای GetHashCode، ToString و یا حتی یک AutoMapper و یا Serializer، برای تمام کلاسهای برنامه، Source Generators میتوانند آنها را به صورت خودکار پیاده سازی کنند و همچنین با هر تغییری در کدهای کلاسها، این پیاده سازیها به صورت خودکار به روز خواهند شد. مزیت این روش نه فقط تولید پویای کدها است، بلکه سبب بهبود کارآیی برنامه هم خواهند شد؛ از این جهت که برای مثال میتوان اعمالی مانند Serialization را بدون انجام Reflection در زمان اجرا، توسط آنها پیاده سازی کرد.
زمانیکه پروسهی کامپایل برنامه شروع میشود، در این بین، به مرحلهی جدیدی به نام «تولید کدها» میرسد. در این حالت، کامپایلر تمام اطلاعاتی را که در مورد پروژهی جاری در اختیار دارد، به تولید کنندهی کد معرفی شدهی به آن ارائه میدهد. بر اساس این اطلاعات غنی ارائه شدهی توسط کامپایلر، تولید کنندهی کد، شروع به تولید کدهای جدیدی کرده و آنها را در اختیار ادامهی پروسهی کامپایل، قرار میدهد. پس از آن، کامپایلر با این کدهای جدید، همانند سایر کدهای موجود در پروژه رفتار کرده و عملکرد عادی خودش را ادامه میدهد.
یک برنامه میتواند از چندین Source Generators نیز استفاده کند که روش قرار گرفتن آنها را در پروسهی کامپایل، در شکل زیر مشاهده میکنید:
Source Generators از یکدیگر کاملا مستقل هستند و اطلاعات آنها Immutable است. یعنی نمیتوان اطلاعات تولیدی توسط یک Source Generator را در دیگری تغییر داد و تمام فایلهای تولیدی توسط انواع Source Generators موجود، به پروسهی کامپایل نهایی اضافه میشوند. هرچند زمانیکه فایلی توسط یک تولید کنندهی کد، به کامپایلر اضافه میشود، بلافاصله اطلاعات آن در کل برنامه و IDE و تمام Source Generators موجود دیگر، قابل مشاهده و استفاده است.
مقایسهای بین تولید کنندههای کد و فناوری IL Weaving
Source Generators، تنها راه و روش تولید کد، نیستند و پیش از آن روشهایی مانند استفاده از T4 templates ، Fody ، PostSharp و امثال آن نیز ارائه شدهاست. در ادامه مقایسهای را بین تولید کنندههای کد و فناوری IL Weaving را که پیشتر در سری AOP در این سایت مطالعه کردهاید، مشاهده میکنید:
تولید کنندههای کد:
- تنها میتوانند فایلهای جدید را اضافه کنند. یعنی «در حین» پروسهی کامپایل ظاهر میشوند و به عنوان یک مکمل، تاثیر گذارند. برای مثال نمیتوانند محتوای یک خاصیت یا متد از پیش موجود را تغییر دهند. اما میتوانند هر نوع کد partial ای را «تکمیل» کنند.
- محتوای اضافه شدهی توسط یک تولید کنندهی کد، بلافاصله توسط Compiler شناسایی شده و بررسی میشود و همچنین در Intellisense ظاهر شده و به سادگی قابل دسترسی است. همچنین، قابلیت دیباگ نیز دارد.
IL Weaving:
- میتوانند bytecode برنامه را تغییر دهند. یعنی «پس از» پروسهی کامپایل ظاهر شده و کدهایی را به اسمبلی نهایی تولید شده اضافه میکنند. در این حالت محدودیتی از لحاظ محل تغییر کدها وجود ندارد. برای مثال میتوان بدنهی یک متد یا خاصیت را بطور کامل بازنویسی کرد و کارکردهایی مانند تزریق کدهای caching و logging را دارند.
- کدهایی که توسط این پروسه اضافه میشوند، در حین کدنویسی متداول، قابلیت دسترسی ندارند؛ چون پس از پروسهی کامپایل، به فایل باینری نهایی تولیدی، اضافه میشوند. بنابراین قابلیت دیباگ به همراه سایر کدهای برنامه را نیز ندارند. به علاوه چون توسط کامپایلر در حین پروسهی کامپایل، بررسی نمیشوند، ممکن است به همراه قطعه کدهای غیرقابل اجرایی نیز باشند و دیباگ آنها بسیار مشکل است.
آیندهی Reflection به چه صورتی خواهد شد؟
هرچند Reflection کار تولید کدی را انجام نمیدهد، اما یکی از کارهای متداول با آن، یافتن و محاسبهی اطلاعات خواص و فیلدهای اشیاء، در زمان اجرا است و مزیت کار کردن با آن نیز این است که اگر خاصیتی یا فیلدی تغییر کند، نیازی به بازنویسی قسمتهای پیاده سازی شدهی با Reflection نیست. به همین جهت برای مثال تقریبا تمام کتابخانههای Serialization، از Reflection برای پیاده سازی اعمال خود استفاده میکنند.
امروز، تمام اینگونه عملیات را توسط Source Generators نیز میتوان انجام داد و این فناوری جدید، قابلیت به روز رسانی خودکار کدهای تولیدی را با کم و زیاد شدن خواص و فیلدهای کلاسها دارد و نمونهای از آن، Source Generator توکار مرتبط با کار با JSON در دات نت 6 است که به شدت سبب بهبود کارآیی برنامه، در مقایسه با استفادهی از Reflection میشود؛ چون اینبار تمام محاسبات دقیق مرتبط با Serialization به صورت خودکار در زمان کامپایل برنامه انجام میشود و جزئی از خروجی برنامهی نهایی خواهد شد و دیگر نیازی به محاسبهی هربارهی اطلاعات مورد نیاز، در زمان اجرای برنامه نیست.
نمونهای از روش دسترسی به اطلاعات کلاسها و خواص و فیلدهای آنها را در زمان کامپایل برنامه توسط Source Generators، در مثال قسمت بعد، مشاهده خواهید کرد.
وضعیت T4 templates چگونه خواهد شد؟
در سالهای آغازین ارائهی دات نت، استفاده از T4 templates جهت تولید کدها بسیار مرسوم بود؛ اما با ارائهی Source Generators، این ابزار نیز منسوخ شده در نظر گرفته میشود.
T4 Templates همانند Source Generators تنها کدها و فایلهای جدیدی را تولید میکنند و توانایی تغییر کدهای موجود را ندارند. اما مشکل مهم آن، داشتن Syntax ای خاص است که توسط اکثر IDEها پشتیبانی نمیشود. همچنین عموما اجرای آنها نیز دستی است و برخلاف Source Generators، با تغییرات کدها، به صورت خودکار به روز نمیشوند.
تغییرات زبان #C در جهت پشتیبانی از تولید کنندههای کد
از سالهای اول ارائهی زبان #C، واژهی کلیدی partial، جهت فراهم آوردن امکان تقسیم کدهای یک کلاس، به چندین فایل، میسر شد که از این قابلیت در فناوری T4 Templates زیاد استفاده میشد. اکنون با ارائهی تولید کنندههای کد، واژهی کلیدی partial را میتوان به متدها نیز افزود تا پیاده سازی اصلی آنها، در فایلی دیگر، توسط تولید کنندههای کد انجام شود. تا C# 8.0 امکان افزودن واژهی کلیدی partial به متدهای خصوصی یک کلاس و آن هم از نوع void وجود داشت و در C# 9.0 به متدهای عمومی کلاسها نیز اضافه شدهاست و اکنون این متدها میتوانند void هم نباشند:
زمانیکه پروسهی کامپایل برنامه شروع میشود، در این بین، به مرحلهی جدیدی به نام «تولید کدها» میرسد. در این حالت، کامپایلر تمام اطلاعاتی را که در مورد پروژهی جاری در اختیار دارد، به تولید کنندهی کد معرفی شدهی به آن ارائه میدهد. بر اساس این اطلاعات غنی ارائه شدهی توسط کامپایلر، تولید کنندهی کد، شروع به تولید کدهای جدیدی کرده و آنها را در اختیار ادامهی پروسهی کامپایل، قرار میدهد. پس از آن، کامپایلر با این کدهای جدید، همانند سایر کدهای موجود در پروژه رفتار کرده و عملکرد عادی خودش را ادامه میدهد.
یک برنامه میتواند از چندین Source Generators نیز استفاده کند که روش قرار گرفتن آنها را در پروسهی کامپایل، در شکل زیر مشاهده میکنید:
Source Generators از یکدیگر کاملا مستقل هستند و اطلاعات آنها Immutable است. یعنی نمیتوان اطلاعات تولیدی توسط یک Source Generator را در دیگری تغییر داد و تمام فایلهای تولیدی توسط انواع Source Generators موجود، به پروسهی کامپایل نهایی اضافه میشوند. هرچند زمانیکه فایلی توسط یک تولید کنندهی کد، به کامپایلر اضافه میشود، بلافاصله اطلاعات آن در کل برنامه و IDE و تمام Source Generators موجود دیگر، قابل مشاهده و استفاده است.
مقایسهای بین تولید کنندههای کد و فناوری IL Weaving
Source Generators، تنها راه و روش تولید کد، نیستند و پیش از آن روشهایی مانند استفاده از T4 templates ، Fody ، PostSharp و امثال آن نیز ارائه شدهاست. در ادامه مقایسهای را بین تولید کنندههای کد و فناوری IL Weaving را که پیشتر در سری AOP در این سایت مطالعه کردهاید، مشاهده میکنید:
تولید کنندههای کد:
- تنها میتوانند فایلهای جدید را اضافه کنند. یعنی «در حین» پروسهی کامپایل ظاهر میشوند و به عنوان یک مکمل، تاثیر گذارند. برای مثال نمیتوانند محتوای یک خاصیت یا متد از پیش موجود را تغییر دهند. اما میتوانند هر نوع کد partial ای را «تکمیل» کنند.
- محتوای اضافه شدهی توسط یک تولید کنندهی کد، بلافاصله توسط Compiler شناسایی شده و بررسی میشود و همچنین در Intellisense ظاهر شده و به سادگی قابل دسترسی است. همچنین، قابلیت دیباگ نیز دارد.
IL Weaving:
- میتوانند bytecode برنامه را تغییر دهند. یعنی «پس از» پروسهی کامپایل ظاهر شده و کدهایی را به اسمبلی نهایی تولید شده اضافه میکنند. در این حالت محدودیتی از لحاظ محل تغییر کدها وجود ندارد. برای مثال میتوان بدنهی یک متد یا خاصیت را بطور کامل بازنویسی کرد و کارکردهایی مانند تزریق کدهای caching و logging را دارند.
- کدهایی که توسط این پروسه اضافه میشوند، در حین کدنویسی متداول، قابلیت دسترسی ندارند؛ چون پس از پروسهی کامپایل، به فایل باینری نهایی تولیدی، اضافه میشوند. بنابراین قابلیت دیباگ به همراه سایر کدهای برنامه را نیز ندارند. به علاوه چون توسط کامپایلر در حین پروسهی کامپایل، بررسی نمیشوند، ممکن است به همراه قطعه کدهای غیرقابل اجرایی نیز باشند و دیباگ آنها بسیار مشکل است.
آیندهی Reflection به چه صورتی خواهد شد؟
هرچند Reflection کار تولید کدی را انجام نمیدهد، اما یکی از کارهای متداول با آن، یافتن و محاسبهی اطلاعات خواص و فیلدهای اشیاء، در زمان اجرا است و مزیت کار کردن با آن نیز این است که اگر خاصیتی یا فیلدی تغییر کند، نیازی به بازنویسی قسمتهای پیاده سازی شدهی با Reflection نیست. به همین جهت برای مثال تقریبا تمام کتابخانههای Serialization، از Reflection برای پیاده سازی اعمال خود استفاده میکنند.
امروز، تمام اینگونه عملیات را توسط Source Generators نیز میتوان انجام داد و این فناوری جدید، قابلیت به روز رسانی خودکار کدهای تولیدی را با کم و زیاد شدن خواص و فیلدهای کلاسها دارد و نمونهای از آن، Source Generator توکار مرتبط با کار با JSON در دات نت 6 است که به شدت سبب بهبود کارآیی برنامه، در مقایسه با استفادهی از Reflection میشود؛ چون اینبار تمام محاسبات دقیق مرتبط با Serialization به صورت خودکار در زمان کامپایل برنامه انجام میشود و جزئی از خروجی برنامهی نهایی خواهد شد و دیگر نیازی به محاسبهی هربارهی اطلاعات مورد نیاز، در زمان اجرای برنامه نیست.
نمونهای از روش دسترسی به اطلاعات کلاسها و خواص و فیلدهای آنها را در زمان کامپایل برنامه توسط Source Generators، در مثال قسمت بعد، مشاهده خواهید کرد.
وضعیت T4 templates چگونه خواهد شد؟
در سالهای آغازین ارائهی دات نت، استفاده از T4 templates جهت تولید کدها بسیار مرسوم بود؛ اما با ارائهی Source Generators، این ابزار نیز منسوخ شده در نظر گرفته میشود.
T4 Templates همانند Source Generators تنها کدها و فایلهای جدیدی را تولید میکنند و توانایی تغییر کدهای موجود را ندارند. اما مشکل مهم آن، داشتن Syntax ای خاص است که توسط اکثر IDEها پشتیبانی نمیشود. همچنین عموما اجرای آنها نیز دستی است و برخلاف Source Generators، با تغییرات کدها، به صورت خودکار به روز نمیشوند.
تغییرات زبان #C در جهت پشتیبانی از تولید کنندههای کد
از سالهای اول ارائهی زبان #C، واژهی کلیدی partial، جهت فراهم آوردن امکان تقسیم کدهای یک کلاس، به چندین فایل، میسر شد که از این قابلیت در فناوری T4 Templates زیاد استفاده میشد. اکنون با ارائهی تولید کنندههای کد، واژهی کلیدی partial را میتوان به متدها نیز افزود تا پیاده سازی اصلی آنها، در فایلی دیگر، توسط تولید کنندههای کد انجام شود. تا C# 8.0 امکان افزودن واژهی کلیدی partial به متدهای خصوصی یک کلاس و آن هم از نوع void وجود داشت و در C# 9.0 به متدهای عمومی کلاسها نیز اضافه شدهاست و اکنون این متدها میتوانند void هم نباشند:
partial class MyType { partial void OnModelCreating(string input); // C# 8.0 public partial bool IsPet(string input); // C# 9.0 } partial class MyType { public partial bool IsPet(string input) => input is "dog" or "cat" or "fish"; }
اشتراکها
کلاینت سبک تلگرام به زبان سی شارپ
اشتراکها
64Bit SIMD Code در دات نت
LINQ یا همان Language-Integrated Query، یک زبان سادهی کوئری نوشتن یکپارچهی با دات نت است. به کمک آن میتوان اعمال پیچیدهای را بر روی اشیاء، به زبانی ساده بیان کرد و امروزه تقریبا توسط تمام توسعه دهندگان دات نت مورد استفاده قرار میگیرد. اما ... این سادگی، بهایی را نیز به همراه دارد: کمتر بودن سرعت اجرا و همچنین افزایش مصرف حافظه. با توجه به گستردگی استفادهی از LINQ، اگر بهبودی در این زمینه حاصل شود، بر روی کارآیی تمام برنامههای دات نتی تاثیر خواهد گذاشت و این امر در دات نت 7 محقق شدهاست. کارآیی متدهای LINQ to Objects در دات نت 7 (مانند متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum) به شدت افزایش یافته و این افزایش گاهی حتی بیشتر از 10 برابر نسبت به نگارشهای قبلی دات نت است؛ اما چگونه به چنین کارآیی رسیدهاند؟
تدارک یک آزمایش برای بررسی میزان افزایش کارآیی متدهای LINQ در دات نت 7
در ادامه یک آزمایش سادهی بررسی کارآیی متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum را با استفاده از کتابخانهی معروف BenchmarkDotNet مشاهده میکنید:
برای آزمایش آن، یکبار target framework پروژه را بر روی net6.0 و بار دیگر بر روی net7.0 قرار داده و برنامه را اجرا میکنیم. خلاصهی مفهومی نتایج حاصل به صورت زیر است که ... شگفتانگیز هستند!
در مورد کار با آرایهها:
- زمان اجرای یافتن Min در آرایههای کوچک، در دات نت 7، نسبت به دات نت 6، حدودا 10 برابر کاهش یافته و اگر این آرایه بزرگتر شود و برای مثال حاوی 10 هزار المان باشد، این زمان 20 برابر کاهش یافتهاست.
- این کاهش زمانها برای سایر متدهای LINQ نیز تقریبا به همین صورت است؛ منها متد Sum که اندازهی آرایه، تاثیری را بر روی نتیجهی نهایی ندارد.
- همچنین در دات نت 7، با فراخوانی متدهای LINQ، افزایش حافظهای مشاهده نمیشود.
در مورد کار با لیستها:
- در دات نت 6، اعمال صورت گرفتهی توسط LINQ بر روی آرایهها، نسبت به لیستها، همواره سریعتر است.
- در دات نت 7 هم در مورد مجموعههای کوچک، وضعیت همانند دات نت 6 است. اما اگر مجموعهها بزرگتر شوند، تفاوتی بین مجموعهها و آرایهها وجود ندارد و حتی وضعیت مجموعهها بهتر است: کارآیی کار با لیستها 32 برابر بیشتر شدهاست!
اما چگونه در دات نت 7، چنین بهبود کارآیی خیرهکنندهای در متدهای LINQ حاصل شدهاست؟
برای بررسی چگونگی بهبود کارآیی متدهای LINQ در دات نت 7 باید به نحوهی پیاده سازی آنها در نگارشهای مختلف دات نت مراجعه کرد. برای مثال پیاده سازی متد الحاقی Min تا دات نت 6 به صورت زیر است:
این متد نسبتا سادهاست. یک IEnumerable را دریافت کرده و سپس با استفاده از متد MoveNext، مقدار فعلی را با مقدار بعدی مقایسه میکند. در این مقایسه، کوچکترین مقدار ذخیره میشود تا در نهایت به انتهای مجموعه برسیم.
اما ... پیاده سازی این متد در دات نت 7 متفاوت است:
در اینجا در ابتدا سعی میشود تا یک ReadOnlySpan از مجموعهی ارائه شده، تهیه شود. اگر این کار میسر نشد، کدهای همان روش قبلی دات نت 6 که توضیح داده شد، اجرا میشود. البته در آزمایشی که ما تدارک دیدیم، چون از لیستها و آرایهها استفاده شده بود، همواره امکان تهیهی یک ReadOnlySpan از آنها میسر است. بنابراین به قسمت اجرایی همانند دات نت 6 نمیرسیم.
اما ... ReadOnlySpan چیست؟ نوعهای Span و ReadOnlySpan، یک ناحیهی پیوستهی مدیریت شده و مدیریت نشدهی حافظه را بیان میکنند. یک Span از نوع ref struct است؛ یعنی تنها میتواند بر روی stack قرار گیرد که مزیت آن، عدم نیاز به تخصیص حافظهی اضافی و بهبود کارآیی است. همچنین ساختار داخلی Span در سی شارپ 11 اندکی تغییر کردهاست که در آن از ref fields جهت دسترسی امن به این ناحیهی از حافظه استفاده میشود. پیشتر از نوع داخلی ByReference برای اشاره به ابتدای این ناحیهی از حافظه استفاده میشد که به همراه بررسی امنیتی در این باره نبود.
پس از دریافت ReadOnlySpan، به سطر زیر میرسیم:
که بررسی میکند آیا سخت افرار فعلی از قابلیتهای SIMD برخوردار است یا خیر؟ اگر بله، اینبار با استفاده از ریاضیات برداری شتاب یافتهی توسط سخت افزار، محاسبات را انجام میدهد:
بنابراین به صورت خلاصه در دات نت 7 با استفاده از بکارگیری نوعهای ویژهی Span و نوعهای برداری شتابیافتهی توسط اکثر سخت افزارهای امروزی، سبب بهبود قابل ملاحظهی کارآیی متدهای LINQ شدهاند.
تدارک یک آزمایش برای بررسی میزان افزایش کارآیی متدهای LINQ در دات نت 7
در ادامه یک آزمایش سادهی بررسی کارآیی متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum را با استفاده از کتابخانهی معروف BenchmarkDotNet مشاهده میکنید:
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using System.Collections.Generic; using System.Linq; [MemoryDiagnoser(displayGenColumns: false)] public partial class Program { static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); [Params (10, 10000)] public int Size { get; set; } private IEnumerable<int> items; [GlobalSetup] public void Setup() { items = Enumerable.Range(1, Size).ToArray(); } [Benchmark] public int Min() => items.Min(); [Benchmark] public int Max() => items.Max(); [Benchmark] public double Average() => items.Average(); [Benchmark] public int Sum() => items.Sum(); }
در مورد کار با آرایهها:
- زمان اجرای یافتن Min در آرایههای کوچک، در دات نت 7، نسبت به دات نت 6، حدودا 10 برابر کاهش یافته و اگر این آرایه بزرگتر شود و برای مثال حاوی 10 هزار المان باشد، این زمان 20 برابر کاهش یافتهاست.
- این کاهش زمانها برای سایر متدهای LINQ نیز تقریبا به همین صورت است؛ منها متد Sum که اندازهی آرایه، تاثیری را بر روی نتیجهی نهایی ندارد.
- همچنین در دات نت 7، با فراخوانی متدهای LINQ، افزایش حافظهای مشاهده نمیشود.
در مورد کار با لیستها:
- در دات نت 6، اعمال صورت گرفتهی توسط LINQ بر روی آرایهها، نسبت به لیستها، همواره سریعتر است.
- در دات نت 7 هم در مورد مجموعههای کوچک، وضعیت همانند دات نت 6 است. اما اگر مجموعهها بزرگتر شوند، تفاوتی بین مجموعهها و آرایهها وجود ندارد و حتی وضعیت مجموعهها بهتر است: کارآیی کار با لیستها 32 برابر بیشتر شدهاست!
اما چگونه در دات نت 7، چنین بهبود کارآیی خیرهکنندهای در متدهای LINQ حاصل شدهاست؟
برای بررسی چگونگی بهبود کارآیی متدهای LINQ در دات نت 7 باید به نحوهی پیاده سازی آنها در نگارشهای مختلف دات نت مراجعه کرد. برای مثال پیاده سازی متد الحاقی Min تا دات نت 6 به صورت زیر است:
public static int Min(this IEnumerable<int> source) { if (source == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } int value; using (IEnumerator<int> e = source.GetEnumerator()) { if (!e.MoveNext()) { ThrowHelper.ThrowNoElementsException(); } value = e.Current; while (e.MoveNext()) { int x = e.Current; if (x < value) { value = x; } } } return value; }
اما ... پیاده سازی این متد در دات نت 7 متفاوت است:
public static int Min(this IEnumerable<int> source) => MinInteger(source); private static T MinInteger<T>(this IEnumerable<T> source) where T : struct, IBinaryInteger<T> { T value; if (source.TryGetSpan(out ReadOnlySpan<T> span)) { if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2) { .... // Optimized implementation return ....; } } .... //Implementation as in .NET 6 }
اما ... ReadOnlySpan چیست؟ نوعهای Span و ReadOnlySpan، یک ناحیهی پیوستهی مدیریت شده و مدیریت نشدهی حافظه را بیان میکنند. یک Span از نوع ref struct است؛ یعنی تنها میتواند بر روی stack قرار گیرد که مزیت آن، عدم نیاز به تخصیص حافظهی اضافی و بهبود کارآیی است. همچنین ساختار داخلی Span در سی شارپ 11 اندکی تغییر کردهاست که در آن از ref fields جهت دسترسی امن به این ناحیهی از حافظه استفاده میشود. پیشتر از نوع داخلی ByReference برای اشاره به ابتدای این ناحیهی از حافظه استفاده میشد که به همراه بررسی امنیتی در این باره نبود.
پس از دریافت ReadOnlySpan، به سطر زیر میرسیم:
if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2)
private static T MinInteger<T>(this IEnumerable<T> source) where T : struct, IBinaryInteger<T> { .... if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2) { var mins = new Vector<T>(span); index = Vector<T>.Count; do { mins = Vector.Min(mins, new Vector<T>(span.Slice(index))); index += Vector<T>.Count; } while (index + Vector<T>.Count <= span.Length); value = mins[0]; for (int i = 1; i < Vector<T>.Count; i++) { if (mins[i] < value) { value = mins[i]; } } .... }
زبان سیشارپ strongly typed و type safe است. کامپایلر بیشتر کد را از نظر صحت نوع (Type) بررسی میکند و در صورت بروز خطا، روند کامپایل متوقف خواهد شد. با این وجود سیشارپ اجازه میدهد که کدهای داینامیک نیز داشته باشیم؛ کدهایی که در زمان کامپایل برای کامپایلر ناشناس هستند و اگر خطای نوع در آنها وجود داشته باشد، در زمان اجرا مشخص شده و باعث توقف برنامه میشود.
Type Safety
ایمنی نوع، قاعدهای است در زبانهای برنامهنویسی که اجازه نمیدهد متغیرها، مقادیری را دریافت کنند که متفاوت با نوع تعریف شدهی آنها باشد. اگر این بررسی وجود نداشت، در زمان اجرا مقادیر خوانده شده از حافظه باعث رفتاری غیر قابل پیشبینی میشد؛ مثلا در یک متغیر عددی، مقدار رشتهای ذخیره و در زمان اجرا با یک مقدار عددی دیگر جمع بسته و نمایش داده شود. کامپایلر همچنین بررسی اعضای اعلان نشدهی متغیرها را نیز انجام میدهد که در قطعه کد زیر آمدهاست:
string text = “String value”; int textLength = text.Length; int textMonth = text.Month; // won’t compile
public interface IGeometricShape { double Circumference { get; } double Area { get; } } public class Square : IGeometricShape { public double Side { get; set; } public double Circumference => 4 * Side; public double Area => Side * Side; } public class Circle : IGeometricShape { public double Radius { get; set; } public double Circumference => 2 * Math.PI * Radius; public double Area => Math.PI * Radius * Radius; } IGeometricShape circle = new Circle { Radius = 1 }; Square square = ((Square)circle); // no compiler error var side = square.Side;
در خط کدی که با کامنت مشخص شده، هر چند که دیده میشود نوع circle نمیتواند به نوع square تبدیل شود، اما این کد بدون خطا کامپایل و خطای InvalidCastException در زمان اجرا رخ خواهد داد. به دلیل اینکه هر دو نوع circle و square از نوع پایه IGeometricShape هستند، کامپایلر خطایی نخواهد گرفت؛ اما در زمان اجرا و زمانیکه برنامه میخواهد اجزاء circle را به square تبدیل کند، مشخص میشود که امکان تبدیل کامل circle به square نیست و خطا رخ خواهد داد.
Dynamic Binding
توسط انقیاد پویا در سیشارپ، کامپایلر بررسی نوع را در زمان کامپایل انجام نخواهد داد. کامپایلر فرض را بر این میگیرد که کد معتبر است و تمام متغیرها به درستی قابل دسترسی هستند. بررسیها در زمان اجرا خواهند بود و زمانی خطا رخ خواهد داد که مثلا دسترسی به یک عضو از یک متغیر امکانپذیر نباشد؛ به این دلیل که آن عضو برای آن نوع وجود ندارد.
توسط کلمه کلیدی dynamic میتوان متغیرهایی را تعریف کرد که در زمان کامپایل از نظر نوع بررسی نشوند؛ مانند مثال زیر.
dynamic text = “String value”; int textLength = text.Length; int textMonth = text.Month; // throws exception at runtime
public dynamic GetAnonymousType() { return new { Name = “John”, Surname = “Doe”, Age = 42 }; } dynamic value = GetAnonymousType(); Console.WriteLine($”{value.Name} {value.Surname}, {value.Age}”);
در مثال بالا نوع بازگشتی متد و متغیری که برای نگهداری نوع بازگشتی تعریف شده از نوع dynamic هستند. هر چند که در زمان کامپایل میشود هر مقداری و نوعی را از متد بازگشت داد، اما مانند مثال قبل، تا زمان اجرا، صحت اینکه آیا واقعا چنین نوعی جهت بازگشت وجود دارد یا نه و همچنین اساسا نوع بازگشت داده شده قابل استفاده و تبدیل هست یا نه، بررسی نخواهد شد. مضاف بر این مشکلات، IntelliSense نخواهیم داشت و اگر بخواهیم از یک اسمبلی دیگر به متد بالا دسترسی پیدا کنیم با خطای RuntimeBinderException مواجه خواهیم شد؛ علت این است که نوعهای anonymous به صورت internal اعلان میشوند. اما میشود استفادههای بهتری از نوع dynamic داشت؛ برای مثال زمان استفاده از کتابخانهی JSON.NET که نمونهای از آن در زیر آمده.
string json = @" { ""name"": ""John"", ""surname"": ""Doe"", ""age"": 42 }"; dynamic value = JObject.Parse(json); Console.WriteLine($"{ value.name} { value.surname}, { value.age}");
مانند نوع anonymous در مثال قبل، متد Parse میتواند مقادیر را به صورت پویا برگشت دهد و میتوان از این مقادیر مانند خصوصیات شیء ایجاد شده، از JSON استفاده کرد، بدون آنکه کامپایلر از وجود آنها اطلاعی داشته باشد. به این ترتیب در زمان اجرا میشود اشیاء JSON را به برنامه داد و از مقادیر آن مانند دسترسی به یک property استفاده کرد؛ کاری که نمیشود با نوعهای anonymous که در مثال بالاتر آورده شد انجام داد. برای حل این مسئله میتوان از دو شیء کمکی در کتابخانه NET Framework. استفاده کرد.
ExpandoObject
بین این دو شیء، ExpandoObject سادهتر است. به همراه کلمه کلیدی dynamic، این شیء اجازه میدهد که به نوع ساخته شده از آن در زمان اجرا و به صورت پویا، عضوی اضافه یا حذف کنیم؛ این اعضا میتوانند متد هم باشند.
dynamic person = new ExpandoObject(); person.Name = "John"; person.Surname = "Doe"; person.Age = 42; person.ToString = (Func<string>)(() => $”{person.Name} {person.Surname}, {person. Age}”); Console.WriteLine($"{ person.Name}{ person.Surname}, { person.Age}");
برای اینکه ببینیم در زمان اجرا چه اعضایی به این شی اضافه شده، میتوان نمونه ساخته شده از آن را به نوع <IDictionary<string, object تبدیل و در یک حلقه به آنها دسترسی پیدا کرد. از همین طریق هم میشود عضوی را حذف کرد.
var dictionary = (IDictionary<string, object>)person; foreach (var member in dictionary) { Console.WriteLine($”{member.Key} = {member.Value}”); } dictionary.Remove(“ToString”);
DynamicObject
از آنجایی که ExpandoObject برای سناریوهای ساده کاربرد دارد و کنترل کمتری بر روی اعضا و نمونههای ایجاد شدهی توسط آن داریم، میتوان از شیء DynamicObject استفاده کرد؛ البته نیاز به کدنویسی بیشتری دارد. پیادهسازی اعضا برای شیء DynamicObject در یک کلاس صورت میگیرد که در زیر آورده شدهاست:
class MyDynamicObject : DynamicObject { private readonly Dictionary<string, object> members = new Dictionary<string, object>(); public override bool TryGetMember(GetMemberBinder binder, out object result) { if (members.ContainsKey(binder.Name)) { result = members[binder.Name]; return true; } else { result = null; return false; } } public override bool TrySetMember(SetMemberBinder binder, object value) { members[binder.Name] = value; return true; } public bool RemoveMember(string name) { return members.Remove(name); } } dynamic person = new MyDynamicObject(); person.Name = “John”; person.Surname = “Doe”; person.Age = 42; person.AsString = (Func<string>)(() => $”{person.Name} {person.Surname}, {person. Age}”);
یک نکته در قطعه کد بالا وجود دارد. در شیء ExpandoObject، متد ToString را اضافه کردیم، اما برای شیء DynamicObject نام آن را تغییر داده و مثلا AsString گذاشتیم. اگر از نام ToString استفاده میکردیم در زمان فراخوانی، متد پیشفرض کلاس DynamicObject فراخوانی میشد. DynamicObject زمانی یک عضو پویا را فراخوانی میکند که آن عضو جدید از قبل وجود نداشته باشد. از آنجا که خود کلاس، متد ToString را دارد متد TryGetMember برای فراخوانی کردن آن اجرا نخواهد شد.
اشتراکها
دسترسی به امکانات SIMD در سیشارپ
اشتراکها