Posted on 8. November 2023

The convenience of System.IO

Read this article in your language IT | EN | DE | ES

Зручність System.IO



Читання та запис файлів дуже поширені, як і інші форми введення-виведення. Файлові API потрібні для читання конфігурації програми, кешування вмісту та завантаження даних (з диска) у пам’ять для виконання деяких обчислень, наприклад (сьогоднішня тема) підрахунок слів. File, FileInfo, FileStreamFile, FileInfo, FileStream, і подібні типи виконують велику роботу для розробників .NET, яким потрібен доступ до файлів. У цій публікації ми розглянемо зручність і продуктивність читання текстових файлів з System.IO з допомогою API System.Text.


Нещодавно ми запустили серію про зручність .NET , яка описує наш підхід до надання зручних рішень для поширених завдань. Зручність System.Text.Json — ще одна публікація в серії про читання та написання документів JSON. Чому .NET? описує варіанти архітектури, які дозволяють використовувати рішення, розглянуті в цих публікаціях.


У цьому дописі аналізується зручність і продуктивність файлового вводу-виводу та текстових API, призначених для підрахунку рядків, слів і байтів у великому романі. Результати показують, що API високого рівня прості у використанні та забезпечують чудову продуктивність, тоді як API нижчого рівня вимагають трохи більше зусиль і забезпечують чудові результати. Ви також побачите, як власний AOT змінює .NET на новий клас продуктивності для запуску програми.

API

Наступні File API (з їхніми супутніми програмами) використовуються в тестах.

1. File.OpenHandle з RandomAccess.Read

2. File.Open з FileStream.Read

3. File.OpenText з StreamReader.ReadіStreamReader.ReadLine

4. File.ReadLines зIEnumerable<string>

5. File.ReadAllLines з string[]

Інтерфейси програмного інтерфейсу (API) перераховані від найвищого контролю до найбільш зручного. Це нормально, якщо вони для вас нові. Це все одно має бути цікаво почитати.


Тести нижчого рівня спираються на такі типи System.Text:

Кодування

Руна

Я також використовував новий клас SearchValues, щоб перевірити, чи дає він значну перевагу перед передачеюSpan<char> в Span<char>.IndexOfAny. Він попередньо обчислює стратегію пошуку, щоб уникнути попередніх витрат IndexOfAny. Спойлер: ефект драматичний.


Далі ми розглянемо програму, яка була реалізована кілька разів — для кожного з цих API — тестування доступності та ефективності.

Застосунок

Програма підраховує рядки, слова та байти в текстовому файлі. Він змодельований на основі поведінки wc, популярного інструменту, доступного в Unix-подібних системах.


Підрахунок слів — це алгоритм, який вимагає перегляду кожного символу у файлі. Підрахунок здійснюється шляхом підрахунку пробілів і розривів рядків.


Слово — це послідовність друкованих символів ненульової довжини, розділених пробілом.


Це з wc --help. Код програми має відповідати цьому рецепту. Здається простим.


Тести підраховують слова в Кларисі Гарлов; або історія молодої леді Семюела Річардсона. Цей текст було вибрано тому, що це, очевидно, одна з найдовших книг англійською мовою та є у вільному доступі на Project Gutenberg . Є навіть його телевізійна адаптація BBC 1991 року.


Я також провів деякі тести з Знедоленими, ще одним довгим текстом. На жаль, 24601 не підійшов через кількість слів.

Результати

Кожна імплементація вимірюється з точки зору:

– Рядків коду

– Швидкості виконання

– Використання пам'яті

Я використовую збірку .NET 8, дуже близьку до остаточної збірки GA. Під час написання цього допису я побачив, що є ще одна збірка .NET 8, однак збірка, яку я використовував, ймовірно, входить до останніх двох чи трьох збірок фінального випуску.


Я використовував BenchmarkDotNet для тестування продуктивності. Це чудовий інструмент, якщо ви ніколи ним не користувалися. Написання контрольного тесту подібне до написання модульного тесту.

 

Наступне використання wc містить список ядер на моїй машині Linux. Кожне ядро ​​отримує свій власний рядок у файлі /proc/cpuinfo з «назвою моделі», яка з’являється в кожному з цих рядків -l і підраховує рядки.

Я використовував цю машину для тестування продуктивності в публікації. Ви бачите, що я використовую Manjaro Linux, який є частиною сімейства Arch Linux. .NET 8 уже доступний у Arch User Repository (який також доступний для користувачів Manjaro).


Рядки коду

Я люблю прості та доступні рішення. Рядки коду є нашою найкращою проксі-метрикою для цього.

На цій діаграмі є два кластери на ~35 і ~75 лініях. Ви побачите, що ці тести зводяться до двох алгоритмів з деякими невеликими відмінностями для адаптації до різних API. Навпаки, wc реалізація трохи довша, близько 1000 рядків. Однак вона робить більше.


Раніше я використовував wc, щоб обчислювати кількість рядків Benchmark знову з -l.

Я написав кілька тестів. Я узагальнив їх на зображенні вище, використовуючи найефективніший тест для кожного файлу API (а потім скоротив назву для простоти). Повний набір тестів буде розглянуто пізніше.


Функціональне співвідношення з wc

 

Давайте переконаємось, що моя реалізація C# відповідає wc.

 

І з count, окремою копією FileOpenHandleCharSearchValuesBenchmark:

Результати фактично ідентичні, з різницею в одне слово в загальній кількості слів. Тут ви бачите версію Linux для wc. Версія для macOS порахувала 985716 слова, три слова відрізняються від реалізації Linux. Я помітив, що у двох файлах є деякі спеціальні символи, які спричиняють ці відмінності. Я не витрачав більше часу на їх вивчення, оскільки це б вийшло за межі цієї публікації.


Перегляньте підсумок(за 10 мікросекунд)

 

Я почав з тестування короткого змісту роману. Це лише 1 кілобайт (9 рядків і 153 слова).

Давайте порахуємо кілька слів.

Я буду називати цей результат нічиєю. Не так багато додатків, де розрив у продуктивності в 1 мікросекунду має значення. Я б не став писати десятки додаткових рядків коду для (лише) цієї перемоги.


Команда byte виграє гонку пам’яті в команди string

Давайте подивимося на використання пам'яті для того самого найменшого документа.

Примітка: 1_048_576  байт — це 1 мегабайт (мебібайт) . 10_000 байтів становить 1% від цього. Примітка. Я використовую формат цілочисельного літералу.


Ви бачите один кластер API, який повертає байти, і інший, який повертає рядки, розподілені у купі. Посередині File.OpenText повертає значення типу char.


File.OpenText покладається на класи StreamReader і FileStream для виконання необхідної обробки. API, що повертають string, покладаються на ті самі типи. Об’єкт StreamReader, який використовується цими API, виділяє кілька буферів, включаючи буфер розміром 1 Кб. Він також створює об’єкт FileStream, який за замовчуванням виділяє буфер 4k. Для File.OpenText(при використанні StreamReader.Read) ці буфери мають постійну вартість, в той час як File.ReadLines та File.ReadAllLines також виділяють рядки (по одному на лінію; змінна вартість).

Швидкість читання книги (за 1 мілісекунду)

 

Давайте подивимося, скільки часу потрібно для підрахунку рядків, слів і байтів у першій частині Клариси Гарлов

Можливо, ми побачимо більший розподіл у продуктивності, продираючись через 610_515 байтів тексту.

API, що повертають byte і char, кластеризуються разом, трохи більше ніж за 1 мс. Ми також бачимо різницю між File.ReadLine і File.ReadAllLines. Однак, слід мати на увазі, що розрив складає лише 2 мс для 600k тексту. Високорівневі API чудово справляються із завданням забезпечення конкурентоспроможної продуктивності за допомогою набагато простіших алгоритмів (у написаному мною коді).


Різницю в File.ReadLine і File.ReadAllLines варто пояснити трохи докладніше.

– Усі API починаються з байтів. File.ReadLines читає значення bytes у char, шукає наступний розрив рядка , потім перетворює цей блок тексту на string, повертаючи по одному .

File.ReadAllLines робить те саме й додатково створює всі рядки string одночасно та пакує їх усі у string[]. Це БАГАТО попередньої роботи, яка потребує багато додаткової пам’яті, яка часто не дає додаткової цінності.


File.OpenText повертає StreamReader, який відкриває API ReadLine та Read. Перший повертає string, а другий одне або інше значення char. Цей параметр ReadLine дуже схожий на використання File.ReadLines, який побудовано на тому ж API. На діаграмі я показав File.OpenText використовуючи StreamReader.Read. Це набагато ефективніше.

Пам'ять: найкраще читати сторінку за раз

Виходячи з різниці у швидкості, ми, ймовірно, також побачимо значні відмінності в пам’яті.

Давайте будемо милосердними. Це драматична різниця. API низького рівня мають фіксовану вартість, тоді як вимоги до пам’яті API string масштабуються відповідно до розміру документа.

 

Написані мною тести FileOpenHandle і FileOpen використовують масиви ArrayPool, вартість якого не відображається в тесті.

Цей код показує два масиви ArrayPool, які використовуються (і їхні розміри). Виходячи зі спостережень, існує значна перевага для продуктивності з буфером 4k і обмеженим (або відсутнім). Буфер 4k здається розумним для обробки файлу 600k.


Я міг би використати приватні масиви (або прийняти буфер від абонента). Моє використання масивів ArrayPool демонструє різницю у використанні пам’яті в основних API. Як бачите, вартість File.Open та File.OpenHandle фактично дорівнює нулю (принаймні, відносно).


Тим не менш, використання пам'яті моїми бенчмарками FileOpen та FileOpenHandle буде дуже схожим на FileOpenText якби я не використовував ArrayPool. Це повинно дати вам уявлення про те, що FileOpenText працює досить добре (якщо не використовувати StreamReader.ReadLine). Звичайно, мою реалізацію можна було б оновити, щоб використовувати набагато менші буфери, але тоді вона працюватиме повільніше.

Паритет продуктивності з wc

Я продемонстрував, що System.IO можна використовувати для отримання тих самих результатів, що й wc. Мені так само слід порівняти продуктивність, використовуючи мій найкращий тест. Тут я використаю команду time для запису всього виклику (від початку до завершення процесу), обробляючи як один том (роману), так і всі томи. Ви побачите, що весь роман (усі 9 томів) складається з понад 5 МБ тексту та трохи менше ніж 1 млн слів.


Почнемо з wc.

Це досить швидко, 9 і 26 мілісекунд.

 

Давайте спробуємо з .NET, використовуючи мою реалізацію

FileOpenHandleCharSearchValuesBenchmark.

Погано! Навіть не близько.

Це 70 і 124 мілісекунди з .NET порівняно з 9 і 26 мілісекундами з wc. Насправді цікаво, що тривалість не залежить від розміру вмісту, особливо з реалізацією .NET. Вартість запуску під час роботи явно домінує.

Всім відомо, що керована мова не може встигати за рідним кодом під час запуску. Цифри це підтверджують. Якби ж ми мали власне кероване середовище виконання.


О! У нас воно є. У нас є нативний АОТ. Давайте спробуємо.


Оскільки мені подобається використовувати контейнери, я використав один із наших образів контейнерів SDK (з монтуванням тому), щоб виконати компіляцію, щоб мені не довелося нативний інструментарій на своїй машині.

Якщо ви уважно придивитесь, то побачите, що програма для порівняння компілюється до < 2 МБ (1_944_896) із рідним AOT. Це середовище виконання, бібліотеки та код програми. Все. Насправді файл символів (count.dbg) більший. Я можу перенести цей виконуваний файл на комп’ютер Ubuntu 22.04 x64, наприклад, і просто запустити його.


Давайте перевіримо нативний AOT.

Маємо 4 і 22 мілісекунди з рідним AOT порівняно з 9 і 25 з  wc. Це чудові результати та досить конкурентоспроможні! Цифри настільки хороші, що мені майже довелося б ще раз перевірити, але підрахунки підтверджують обчислення.

Примітка. Я налаштував програму за допомогою <OptimizationPreference>Speed</OptimizationPreference>. Це дало невелику користь.

Текст, руни та Юнікод

Текст є всюди. Насправді ви його зараз читаєте. .NET містить декілька типів для обробки та зберігання тексту, зокрема Char, Encoding, Rune та String.


Юнікод кодує понад мільйон символів, у тому числі емодзі . Перші 128 символів ASCII і Unicode збігаються. Є три кодування Unicode : UTF8, UTF16 і UTF32, із різною кількістю байтів, які використовуються для кодування кожного символу.


Ось трохи (напіввідповідного) тексту з «Гобіта».


— Місячні літери — це рунічні літери, але їх не видно, — сказав Елронд

Я не можу не думати, що місячні літери є фантастичними пробілами .

 

Ось результати невеликої утиліти , яка друкує інформацію про кожен символ Unicode, використовуючи цей текст. Довжина байтів і байти є специфічними для представлення UTF8.

Для кодування початкового символу “лапки” потрібно три байти. Для всіх символів, що залишилися, потрібен один байт, оскільки вони знаходяться в діапазоні символів ASCII. Ми також бачимо один пробіл - символ пробілу.


Двійкове представлення символів, які використовують однобайтове кодування, точно відповідає їхнім цілим значенням кодової точки. Наприклад, двійкове представлення кодової точки «M» (77) –  0b01001101 таке ж, як ціле число 77. Навпаки, двійкове представлення цілого числа 8220 є 0b_100000_00011100, а не трибайтове двійкове значення, яке ми бачимо вище для . Це тому, що кодування Unicode описує більше, ніж просто значення кодової точки .

 

Ось ще одна програма , яка має надати ще більше розуміння .

Він виводить наступне:

 

Я можу знову запустити програму, змінивши кодування на UTF16. Я змінив значення encoding на Encoding.Unicode.

Це вказує нам на кілька речей:

– Кодування UTF8 має нерівномірне кодування байтів.

– Кодування UTF16 більш однорідне.

– Символи, для яких потрібна одна кодова точка, можуть взаємодіяти з int, створюючи такі шаблони, як (char)8220 або(char)0x201C.

– Символи, які вимагають двох кодових точок, можна зберігати в string, цілочисельному значенні (UTF32) або як Rune, дозволяючи шаблони, такі як (Rune)128512.

– Легко писати програмне забезпечення з помилками, якщо код безпосередньо обробляє символи або (що ще гірше) байти. Наприклад, уявіть собі, що ви пишете алгоритм текстового пошуку, який підтримує терміни пошуку emoji .

– Багатокодових символів достатньо, щоб запустити будь-якого розробника.

– Мій термінал підтримує emoji (і я цьому дуже радий).


Ми можемо зв’язати ці концепції Unicode з типами .NET.

string та char використовують кодування UTF16.

– Класи Encoding дозволяють обробляти текст між кодуваннями та значеннями byte.

string підтримує символи Unicode, які потребують однієї або двох кодових точок.

Rune може представляти всі символи Unicode (включаючи сурогатні пари), на відміну від char.


Усі ці типи використовуються в тестах. Усі контрольні тести (крім одного, який обманює) належним чином використовують ці типи, щоб правильно обробляти текст Unicode.


Давайте подивимося на контрольні показники.


File.ReadLines та File.ReadAllLines

Наступні контрольні тести реалізують високорівневий алгоритм на основі рядків string:

FileReadLines

FileReadAllLinesBenchmark

 

Діаграми продуктивності в розділі результатів містять обидва ці контрольні показники, тому немає потреби знову показувати ці результати.


Показник FileReadLines встановлює основу для нашого аналізу. Він використовує foreach понад IEnumerable<string>.

}

Код підраховує рядки та символи через зовнішній foreach. Внутрішній foreach підраховує слова після пробілів, дивлячись на кожен символ у рядку. Він використовує char.IsWhiteSpace для визначення того, чи є символ пробілом. Цей алгоритм настільки ж простий, як і для підрахунку слів.

Примітка. Додаток запускається в тестах кількома різними способами, для мого власного тестування. Це причина дивних аргументів командного рядка.


Результати в основному відповідають інструменту wc. Кількість байтів не збігається, оскільки цей код працює з символами, а не з байтами. Це означає, що позначки порядку байтів , багатобайтові кодування та символи завершення рядка були приховані від очей. Я міг би додати +1 до charCount кожного рядка, але це не здалося мені корисним, особливо тому, що існує кілька схем нового рядка . Я вирішив точно підрахувати символи або байти і не намагатися приблизно оцінити різницю між ними.


Підсумок: ці API чудово підходять для невеликих документів або коли використання пам’яті не є сильним обмеженням. Я б використовував, лише File.ReadAllLines якщо б мій алгоритм покладався на знання кількості рядків у документі наперед і лише для невеликих документів. Для великих документів я б застосував кращий алгоритм для підрахунку символів розриву рядка, щоб уникнути використання цього API.

File.OpenText

У наведених нижче контрольних тестах реалізовано різноманітні підходи, усі вони базуються на StreamReader, в якому File.OpenText є просто оболонкою. Деякі з API StreamReader відображають рядки string, а інші – значення char. Саме тут ми побачимо більшу різницю в продуктивності.

FileOpenTextReadLineBenchmark

FileOpenTextReadLineSearchValuesBenchmark

FileOpenTextCharBenchmark

FileOpenTextCharLinesBenchmark

FileOpenTextCharIndexOfAnyBenchmark

FileOpenTextCharSearchValuesBenchmark


Метою цих бенчмарок є визначення переваг SearchValues і переваг char порівняно з string Я також включив показник FileReadLinesBenchmark як базовий із попереднього набору контрольних показників.

Ви можете запитати про пам’ять. Використання пам’яті StreamReader є функцією char vs string, яку ви можете побачити на початкових діаграмах пам’яті раніше в публікації. Відмінності в цих алгоритмах впливають на швидкість, але не на пам'ять.


Бенчмарк FileOpenTextReadLineBenchmark фактично ідентичний  FileReadLines, тільки без абстракції IEnumerable<string>


Бенчмарк FileOpenTextReadLineSearchValuesBenchmark починає ставати трохи привабливішим.

Цей тест просто підраховує пробіли (які він не обрізає). Він використовує переваги нового типу SearchValues, який може пришвидшити IndexOfAny при пошуку не лише кількох значень. Об’єкт SearchValues створено з пробілами, крім (більшості) символів розриву рядка. Ми можемо припустити, що символи розриву рядка більше не присутні, оскільки код покладається на StreamReader.ReadLine.


Я міг би використати той самий алгоритм для попередніх реалізацій тестів, однак я хотів зіставити найбільш доступні API з найбільш доступними реалізаціями тестів.


Значною частиною причини, чому IndexOfAny так добре працює, є векторизація.

.NET 8 містить векторні API аж до 512 біт. Ви можете використовувати їх у власних алгоритмах або покластися на вбудовані API, щоб IndexOfAny отримав перевагу від покращеної обчислювальної потужності. Зручне API IsHardwareAccelerated повідомляє вам, наскільки великі векторні регістри на даному ЦП. Це результат на моїй машині Intel. Я експериментував із новим апаратним забезпеченням Intel, доступним у Azure, яке повідомляло Vector512.IsHardwareAccelerated як True. Моя машина MacBook M1 повідомляє Vector128.IsHardwareAccelerated як найвищу доступну.


Тепер ми можемо залишити землю string і перейти до значення char. Є дві очікувані великі переваги. Перша полягає в тому, що базовому API не потрібно читати наперед, щоб знайти символ розриву рядка, і більше не буде рядків для розподілу купи та збору сміття. Ми повинні побачити помітне покращення швидкості, і ми вже знаємо з попередніх діаграм, що є значне зменшення пам’яті.


Я побудував наступні тести, щоб оцінити цінність різних стратегій.

FileOpenTextCharBenchmark — Той самий базовий алгоритм, що й FileReadLines із додаванням перевірки розривів рядків.

FileOpenTextCharLinesBenchmark — Спроба спростити основний алгоритм шляхом синтезу рядків символів.

FileOpenTextCharSearchValuesBenchmark — Подібне використання SearchValues як FileOpenTextReadLineSearchValuesBenchmark для прискорення пошуку простору, але без попередньо обчислених рядків.

FileOpenTextCharIndexOfAnyBenchmark — Точно такий самий алгоритм, але який використовує IndexOfAny з Span<char> замість нових типів SearchValues.


Ці бенчмарки (як показано на діаграмі вище) говорять нам про те, що використовувати IndexOfAny з SearchValues<char> дуже корисно. Цікаво подивитися, наскільки погано IndexOfAny працює, якщо дано стільки значень (25) для перевірки. Виходить набагато повільніше, ніж просто перемикання кожного символу з перевіркою char.IsWhiteSpace. Ці результати мають зупинити вас, якщо ви використовуєте великий набір пошукових термінів із IndexOfAny.


Я провів деякі тести на деяких інших машинах. Я помітив, що FileOpenTextCharLinesBenchmark працює досить добре на машині AVX512 (з нижчою тактовою частотою). Можливо, це пов’язано з тим, що він більшою мірою покладається на IndexOfAny (тільки з двома пошуковими термінами) і в іншому випадку є досить економним алгоритмом.


Ось імплементація FileOpenTextCharSearchValuesBenchmark.

Це не дуже відрізняється від оригінальної реалізації. Перший блок повинен враховувати розриви рядків у перевірці  char.IsWhiteSpace. Після цього IndexOfAny використовується з SearchValue<char> для пошуку наступного пробілу, щоб можна було виконати наступну перевірку. Якщо IndexOfAny повертає -1, ми знаємо, що пробілів більше немає, тому нема потреби читати далі буфер.


Span<T> повсюдно використовується  в цій реалізації. Проміжки забезпечують дешевий спосіб створення вікон у базовому масиві. Вони настільки дешеві, що реалізація може продовжувати нарізку аж до тих пір, поки chars.Length > 0 більше не буде правдивим. Я використовував цей підхід лише з алгоритмами, які вимагали фрагментів >1 символів одночасно. В іншому випадку я використовував цикл for для повторення Span, що було швидше.


Примітка: Visual Studio запропонує, що chars.Slice(1) можна спростити до chars[1..]. Я виявив, що спрощення не є еквівалентним і відображається як регресія продуктивності в контрольних тестах. Набагато менша ймовірність виникнення проблеми в програмах.

Бенчмарки FileOpenTextChar* набагато ближчі до відповідності з wc для байтових результатів (для тексту ASCII). Позначка порядку байтів (BOM) споживається, перш ніж ці API починають повертати значення. Як наслідок, кількість байтів для API, що повертають char, постійно змінюється на три байти (розмір специфікації). На відміну від API, що повертають string, тут враховуються всі символи розриву рядка.



Підсумок: StreamReader (який є основою File.OpenText) пропонує гнучкий набір API, що охоплює широкий діапазон доступності та продуктивності. Для більшості випадків використання (якщо File.ReadLines не підходить) StreamReader це чудовий вибір за замовчуванням.

File.Open та File.OpenHandle

У наведених нижче тестах реалізовано алгоритми найнижчого рівня на основі байтів. File.Open є оболонкою на FileStream. File.OpenHandle повертає дескриптор операційної системи, до якого потрібен RandomAccess.Read для доступу.

FileOpenCharSearchValuesBenchmark

FileOpenHandleCharSearchValuesBenchmark

FileOpenHandleRuneBenchmark

FileOpenHandleAsciiCheatBenchmark


Ці API пропонують набагато більше контролю. Рядки та символи тепер зникли, і ми залишилися з байтами. Ціль цих тестів — отримати найкращу можливу продуктивність і дослідити засоби для правильного читання тексту Unicode, враховуючи, що API повертає байти.

Кількість байтів тепер збігається. Тепер ми переглядаємо кожен байт у даному файлі.

FileOpenHandleCharSearchValuesBenchmark додає кілька нових концепцій. FileOpenCharSearchValuesBenchmark фактично ідентичний.

Остання спроба порівняти результати wc.

Основа цього алгоритму фактично ідентична реалізації FileOpenTextCharSearchValuesBenchmark, яку ми щойно бачили. Що відрізняється, так це початкове налаштування.

Наступні два блоки коду є новими.

Цей код отримує декодер UTF8 для перетворення байтів на символи. Він також отримує максимальну кількість символів, яку може створити декодер, враховуючи розмір байтового буфера, який буде використовуватися. Ця реалізація жорстко закодована для використання UTF8. Його можна зробити динамічним (прочитавши позначку порядку байтів), щоб використовувати інші кодування Unicode.

Цей блок декодує буфер байтів у символьний буфер. Обидва буфери мають правильний розмір (з AsSpan) відповідно до повідомлених підрахованих значень byte та char. Після цього код приймає більш звичний алгоритм char. Немає очевидного способу використання SearchValues<byte>, який добре поєднується з багатобайтовим кодуванням Unicode. Цей підхід добре працює, тому це не має великого значення.


Ця публікація про зручність. Я виявив, що Decoder.GetChars це неймовірно зручно. Це чудовий приклад низькорівневого API, який робить саме те, що потрібно, і начебто рятує день у окопах. Я знайшов цю закономірність, прочитавши, як File.ReadLines (опосередковано) вирішує саме цю проблему. Весь цей код можна переглянути. Це відкритий код!


FileOpenHandleRuneBenchmark використовує клас Rune замість Encoding. Це виглядає повільніше, частково тому, що я повернувся до більш базового алгоритму. Не було очевидно, як використовувати IndexOfAny або SearchValues з Rune, частково тому, що не існує аналога decoder.GetChars для Rune.

Тут немає особливої ​​різниці, і це добре. Rune значною мірою замінює char.

 

Цей рядок є ключовою відмінністю.

Мені потрібен API, який повертає символ Unicode зSpan<byte> та повідомляє про кількість прочитаних байтів. Це може бути від 1 до 4 байтів. Rune.DecodeFromUtf8 робить саме це. Для моїх цілей мені байдуже, чи отримаю я назад Rune чи char. Вони обидві є структурами.


Я залишив FileOpenHandleAsciiCheatBenchmark наостанок. Я хотів побачити, наскільки швидше можна було б змусити код працювати, якщо він міг застосувати максимальну кількість припущень. Коротше кажучи, як би виглядав алгоритм лише ASCII?

 

Цей код майже ідентичний тому, що ви бачили раніше, за винятком того, що він шукає набагато менше символів, що — СЮРПРИЗ — прискорює роботу алгоритму. Ви можете побачити це на діаграмі раніше в цьому розділі. SearchValues тут не використовується, оскільки він не оптимізований лише для двох значень.

Цей алгоритм все ще здатний давати очікувані рез

ультати. Це лише тому, що текстовий файл задовольняє припущення коду.

Підсумок: File.Openі File.OpenHandleпропонують найвищий контроль і продуктивність. У випадку текстових даних неочевидно, що варто докладати додаткових зусиль над File.OpenTextchar), навіть якщо вони можуть забезпечити вищу продуктивність. У цьому випадку ці API повинні відповідати базовій лінії підрахунку байтів. Для нетекстових даних ці API є більш очевидним вибором.

Резюме

System.IO надає ефективні API, які охоплюють багато випадків використання. Мені подобається, як легко створювати прості алгоритми за допомогою File.ReadLines. Це працює дуже добре для вмісту, який базується на рядках. File.OpenText дозволяє писати швидші алгоритми без значного ускладнення. Нарешті,  File.Open  таFile.OpenHandle чудово підходять для отримання доступу до двійкового вмісту файлів і для написання найбільш високопродуктивних і точних алгоритмів.


Я не збирався досліджувати API глобалізації .NET або Unicode настільки глибоко. Раніше я використовував API кодування, але ніколи не пробував Rune. Я був вражений тим, наскільки добре ці API підходять для мого проєкту та наскільки добре вони можуть працювати. Ці API були несподіваним прикладом зручності публікації. Зручність означає не «високий рівень», а «правильний і доступний інструмент для роботи».


Інше розуміння полягало в тому, що для вирішення цієї проблеми високорівневі API були доступними та ефективними, однак лише низькорівневі API були здатні точно відповідати результатам wc. Я не розумів цієї динаміки, коли починав проєкт, однак я був радий, що необхідні API були цілком доступні.

 

Source








Exception: Stack empty.
Comments are closed