Обновить
13
Сергей Пономарев@novar

Программист

5
Подписчики
Отправить сообщение
я так понял, что требовалось не убрать ожидание и блокировки (которые неизбежны при получении асинхронно поступающих элементов), а сделать ожидание асинхронным
а родной Microsoft TPL Dataflow чем не угодил?
совершенно очевидно, что вызов уже отписанного обработчика надёжно предотвратить может только сам обработчик. ведь отписка может конкурентно произойти как раз в момент собственно вызова. таким образом, в источнике событий нам остаётся только проверка на пустой/null делегат
Спасибо за указание на возможные проблемы, поправил «дерзкие» имена классов. Collection -> ReadOnlyCollection, FiniteSet -> ReadOnlyFiniteSet, List -> ReadOnlyList. Теперь выглядит немного сложнее: List.Empty<>() будет ReadOnlyList.Empty<>() зато меньше вероятность коллизий.
добавил в проект, как обещал, тесты и nuspec
1. API фиксирванный (диктуется LINQ), поэтому никто удалять или добавлять параметры не будет.
2. Про установку и настройку инструментов. Я считаю, что не имею права заставлять людей это делать просто чтобы скомпилировать мой проект.
3. Согласен, в нескольких аналогичных местах можно съэкономить пяток строк кода, считаю это неприпинципиально. Сделаю в свободное время.
Про опциональные параметры я в курсе. И проштудировал множество рекомендаций и обсуждений. Авторитетные для меня люди утверждают, что допустимо использовать совершенно очевидное значение (null для ссылочных типов), которое не может измениться ни в какой мыслимой ситуации.

Выбор между «if () throw» и Contract.Requires() сделан согласно рекомендациям официального руководства по Code Contracts (см. раздел 5 Usage Guidelines). Это единственный вариант не требующий установки и настройки отдельного инструментария и в тоже время позволяющий создавать отдельные конфигурации прокта и использовать в них контракты для статического анализа.

Не вижу ни одного места где мог бы использовать что-то готовое из LINQ. Судя по его исходникам мог бы использовать кое что, но оно помечено как internal.
ну я так и написал:
В «LINQ к объектам» уже реализована подобная внутренняя оптимизация: многие методы первым делом проверяют реализацию некоторых интерфейсов входной коллекцией, и используют их для более эффективного выполнения действий.

Но, всё таки, даже «старые» полные интерфейсы используются в LINQ не всегда. Например, в методе Reverse интерфейс IList<> не проверяется и всегда создаётся полная копия всей коллекции. А уж возвращаемая коллекция практически никогда не реализует никаких интерфейсов кроме базового IEnumerable<>, в связи с чем следующий метод в цепочке опять будет всё перебирать и создавать копию.
Для сравнения, в моей CollecitonsLinq для коллекции типа IReadOnlyList<> в методе Reverse() будет возвращён непосредственный декоратор типа IReadOnlyList<> без перебора и копирования.

А про операцию Where я отметил, что в ней улучшать нечего и предлагаю использовать такую, как есть.
IReadOnlyFiniteSet не имеет прямого отношения к этой библиотеке, поэтому в корне. неясно делать ли его частью nuget-пакета или отдельно. вариант nuget-пакета с ним в комплекте тут www.nuget.org/packages/CollectionLinq

тесты и nuspec есть, добавлю в проект сегодня попозже
Само перечисление с использование «утиной типизации» было создано до появления обобщённых (дженерик) типов, а созданные позднее библиотечные коллекции просто следуют заложенному паттерну для совместимости со старыми не обобщёнными программами (а не для производительности). Что и порождает до сих пор множественные проблемы (например, вот свежее обсуждение Why does my enumerator not advance?). Уверен, что если бы в языке изначально были обобщённые типы, то «утиная типизация» не использовалась бы для foreach. Там, где нужна экстремальная производительность всегда используют массивы и for, а не перечислители и foreach.

Может я не заметил каких то предостережений в вашей статье, но вот прямо в начале написано: «На мой взгляд, есть смысл использовать наши хаки в следующих случаях: Вы пишете новый код и решили делать это сразу круто и с экономией.» Использовать анти-паттерны при написании нового (то есть ещё даже не профилированного) кода — по моему вредный совет.
По пункту 1 про сравнение строк — это детали реализации. Вполне возможно, что на другой платформе или даже на той же, но после какого нибудь мелкого обновления, результат сравнения производительности радикально изменится. Но в предлагаемом решении хотя бы нет минусов: всегда полезно указать какой именно тип сравнения используется.

По пункту 2 (про перечислители) — категорически не согласен. Ради очень сомнительной нано-оптимизации (одно выделение ссылочного объекта на весь цикл) нам рекомендуют применять сразу три анти-паттерна (изменяемый тип-значение, конкретный тип вместо интерфейса, более конкретный интерфейс вместо общего). В библиотечных классах перечислитель сделан в виде типа-значения в давние времена, до появления обобщённых (дженерик) типов, чтобы избегать оборачивания (боксинга) в каждой итерации цикла. В наше время это неактуально.

По пункту 3 про создание делегатов. Опять сомнительная нано-оптимизация. Делегаты ведь фигурируют обычно только один раз, при инициализации. И добавлять для этого отдельный член класса не забыв его правильно инициализировать — нет смысла, мы лишь добавим вероятность появления различных опечаток в коде. И опять же, никто не может быть уверен что в следующем обновлении фреймворка такая оптимизация не будет производится компилятором автоматически.

Итог: не применяйте оптимизации п.2 и 3 до тех пор, пока профилирование не покажет вам что именно они могут дать заметный прирост производительности.
Прежде всего, конечно же, надо было начинать с изложения официальной точки зрения Design Guidelines for Exceptions плюс Best Practices for Exceptions. Также очень рекомендую рассуждения Vexing exceptions от Eric Lippert (создателя компилятора C#). Без этого получилось слишком много сомнительной «отсебятины».

По поводу указанных требований «класс должен быть помечен атрибутом [System.Serializable]» и «класс должен определять конструктор для поддержки сериализации типа». Это некоторый пережиток прошлого и в современных библиотеках, которые делают как Portable Class Libraries, они неактуальны.

В примере, описываемом в начале статьи, не нужно создавать своё исключение, тут идеально подходит InvalidOperationException, которое будет означать неверную операцию «редактирование несуществующего пользователя».

По поводу упоминающегося в комментариях ArgumentException. Это исключение (и все производные от него) соверешенно особенное, оно означает нарушение контракта. Оно никогда не должно возникать при выполнении программ. Его возникновение должно быть 100% предсказуемо до вызова метода, на основании значений параметров и свойств объекта согласно контракту метода. Это исключение означает неправильно написанную программу.
С точки зрения простого потребления данных, да, именно так. Но также важна возможность создавать вложенные и трансформирующие источники.
Да, такая схема работает с PNG или с любым другим источником одной структуры, а для каждого другого вида структур придётся иметь свой объект Chunk. В такой схеме первичный парсер должен быть жёстко связан со всеми последующими парсерами, что делает невозможным создание универсальных парсеров, пригодных для использования в любых проектах.
А моё решение как раз даёт возможность делать универсальные компоненты-парсеры, которые получают на вход BufferedSource, а также могут делегировать часть парсинга другим независимым компонентам.
Благодаря взаимодействия с вами, добавил в статью абзац, уточняющий что моё решение не меняет модель (паттерн) потребления данных из Stream и не применимо для других моделей.
«Не оглядываясь на других потребителей» конечно же, всегда верно только для чтения в буфер. А потребление из буфера подчиняется логике компонентов и должно быть учтено на их уровне в тот момент, когда один компонент, получив на вход BufferedSource, передаёт его (с какой то трансляцией) другим компонентам.
Мешает то, что я не знаю сколько их будет пока не прочитаю и найду признак окончания порции.
Тут я не согласен. Stream не разделяет понятие «считать, сделав данные доступными» и «потребить, убрав из доступа» (в нём это одна неделимая операция), что и является причиной невозможности потреблять один и тотже поток в разных компонентах. Эта проблема решается в BufferedSource, давая возможность считывать данные произвольно большими кусками, потребляя только то, что нужно каждому компоненту и не оглядываясь на присутствие других потребителей. Ваша задача вообще красиво не решаема в модели когда потребители запрашивают данные, тут нужна модель когда источник проталкивает данные. Такая модель совершенно ортогональна моим наработкам и может быть добавлена сверху без каких либо трудностей и потерь производительности.
При работе с реальным I/O может и не будет существенного влияния на суммарное время выполнения (зато будет влияние на % загрузки процессора). Но ведь наш источник универсален, в том числе может предоставлять данные из ОЗУ (по аналогии с MemoryStream) или из уже считанных в буфер данных другого источника (при крипто-преобразовании) и тут влияние будет значительным.
Чтобы пояснить неудобство метода Read() привожу пример со Stream:
var buf = new byte[1024];
s.Read (buf, 0, 1024);
int bufIdx = 0;
// тут каким то образом мы обработали первые 1021 байт в буфере, осталось 3. bufIdx == 1021
// далее нам надо взять 32-х битное число, то есть 4 байта
if (bufIdx > (1024-4))
{
  Array.Copy (buf, bufIdx, buf, 0, 1024-bufIdx);
  if (s.Read (buf, 1024-bufIdx, bufIdx) < 4) throw new InvalidOperationException ("не хватило данных");
  bufIdx = 0;
}
BitConverter.ToInt32 (buf, bufIdx)

и аналог c BufferedSource:
s.FillBuffer ();
// тут каким то образом мы пропустили первые 1021 байт в буфере, осталось 3
// далее нам надо взять 32-х битное число, то есть 4 байта
s.EnsureBuffer (4);
BitConverter.ToInt32 (s.Buffer, s.Offset)

и это надо повторять для КАЖДОГО получения блока байтов.
Суть моей задумки как раз в том, чтобы в независимости от компонента, потреблялся один источник с единственным буфером. То есть когда один потребитель что то осознанно забирает из источника (не так как из Steam забирают просто весь буфер и неизвестно что действительно понадобится), то эти потреблённые данные также исчезают из доступных данных всех других компонентов, работающих с этим источником (или его родительским источником). Ваш пример, напротив, предполагает, что один из компонентов не должен потреблять данные источника, а должен только производить дополнительные действия с потребляемыми другими компонентами данными. Решение — это один из потребителей сделать источником-ретранслятором, методы которого напрямую ретранслируются в наш источник, но при каждом потреблении данных дополнительно что то делает с ними (пишет на диск как вы хотели). Можно взять мой ObservableBufferedSource но вместо вызова _progress.Report() сделать запись на диск. Код получится примерно такой:
//s - это некий IBufferedSource, переданный снаружи
var fileWritingSource = new FileWritingBufferedSource (s, имя_файла);
while(fileWritingSource.FillBuffer() > 0)
{
   //пишем полученные данные в БД
   db.WriteData (fileWritingSource.Buffer, fileWritingSource.Offset, fileWritingSource.Count);
   hashingSource.SkipBuffer (fileWritingSource.Count);
}
Второй указанный мной способ предполагает доступ сразу к большому количеству данных (буферу) и простейшую арифметическую проверку индекса без вызова методов источника. А вызов метода источника для КАЖДОГО байта (пусть даже из буфера) при считывании мегабайтов будет неприемлемо медленным. Хотя для некоторых задач с небольшими источниками, возможно производительность будет приемлема, но мы же хотим универсальное решение?

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность