Уточнить длительные операции Linq?

Я задал вопрос и получил ответы о проблемах с производительностью, которые у меня были с большим набором данных. (созданный с помощью linq)

ok, оставьте его в стороне.

Но одна из интересной (и гениальной) оптимизации, предложенная Марком, заключалась в Batchify запросе linq.

/*1*/ static IEnumerable<T> Batchify<T>(this IEnumerable<T> source, int count)
/*2*/    {
/*3*/      var list = new List<T>(count);
/*4*/      foreach(var item in source)
/*5*/         {
/*6*/           list.Add(item);
/*7*/           if(list.Count == count)
/*8*/             {
/*9*/               foreach (var x in list) yield return x;
/*10*/              list.Clear();
/*11*/            }
/*12*/        }
/*13*/      foreach (var item in list) yield return item;
/*14*/    }

Здесь цель Batchify - гарантировать, что мы не помогаем сервера слишком много, занимая значительное время между каждой операцией - данные изобретаются партиями по 1000 и каждая партия производится доступный очень быстро.

Теперь я понимаю, что он делает, но я не могу сказать разницы, так как я могу пропустить то, как это работает. (иногда вы думаете, что знаете что-то... до...)

ОК, вернемся к основам:

AFAIK, Linq работает как эта цепочка -:

enter image description here

Итак, мы не можем начать перечисление до результата select в:

Where-->OrderBy-->Select 

.

Итак, в основном я жду, когда select будет иметь все правильные данные (после where, после orderby), и только тогда - мой код может коснуться этих значений. (полученная из select)

Но, согласно моему пониманию ответа Marc, кажется, что между этими yields есть пробел, который позволяет другим ресурсам что-то делать... (?)

Если да, то между каждой итерацией #4 после строки #9 есть время, когда CPU может сделать что-то еще?

Вопрос

  • Может кто-нибудь пролить свет, пожалуйста? как это работает?

пь

Я уже знаю, что (например) select не что иное, как:

public static IEnumerable<TResult> Select<TSource,TResult>
 (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
   foreach (TSource element in source)
       yield return selector (elem![enter image description here][3]ent);
}

Но если это так, мой код не может касаться его до тех пор, пока не будут рассчитаны все значения (после where, orderby)...

изменить:

Для тех, кто спрашивает, есть ли разница: http://i.stack.imgur.com/19Ojw.jpg

2 секунды для элементов 1M. 9 секунд для элементов 5M.

(игнорируйте вторую строку времени (дополнительная строка console.write).) enter image description here

вот он для списка 5 м: http://i.stack.imgur.com/DflGR.jpg (первый из них withBatchify, другой нет)

enter image description here

Ответы

Ответ 1

Важно: показанное изображение включает OrderBy: вы должны заметить, что это прерывает здесь batchify, потому что OrderBy является буферизированным оператором. Метод batchify, который я показал, предназначен для потоков буферизации без буферизации.

В контексте, в котором я использовал его, начало (до batchify) было блоком итератора, которое делало множество вещей, связанных с созданием объектов и генераторами псевдослучайных чисел на каждой итерации. Поскольку этот код был чувствительным к срокам, то, что я не хотел делать, это представить надежную паузу (для работы ЦП по созданию каждого элемента) между каждым вызовом в хранилище. Это было отчасти для эмуляции исходного кода, который создал все объекты спереди, и был отчасти потому, что я понимаю, как SE.Redis обрабатывает работу сокета.

Рассмотрим поведение без Batchify:

  • создать элемент (работа ЦП) и получить его
  • отправить его в магазин (сеть IO)
  • создать элемент (работа ЦП) и получить его
  • отправить его в магазин (сеть IO)
  • создать элемент (работа ЦП) и получить его
  • отправить его в магазин (сеть IO)
  • ...

В частности, это означает, что существует предсказуемая пауза между запросами магазина. SE.Redis обрабатывает сокет IO на выделенном рабочем потоке, и вышеизложенное может довольно легко привести к высокой фрагментации пакетов, тем более, что я использовал флаг "огонь и забыть". Пишущий поток необходимо периодически очищать, что происходит, когда либо буфер достигает критического размера, либо больше не работает в очереди исходящих сообщений.

Теперь рассмотрим, что делает batchify:

  • создать элемент (работа ЦП) и буферизировать его
  • создать элемент (работа ЦП) и буферизировать его
  • ...
  • создать элемент (работа ЦП) и буферизировать его
  • введите элемент
  • отправить его в магазин (сеть IO)
  • введите элемент
  • отправить его в магазин (сеть IO)
  • ...
  • введите элемент
  • отправить его в магазин (сеть IO)
  • создать элемент (работа ЦП) и буферизировать его
  • ...

здесь вы можете с уверенностью видеть, что процессорное усилие между запросами хранилища значительно уменьшено. Это более корректно имитирует исходный код, в котором сначала был создан список миллионов, а затем повторялся. Но, кроме того, это означает, что существует очень хороший шанс, что поток, создающий исходящие сообщения, может идти как минимум так же быстро, как поток записи, что означает, что исходящая очередь вряд ли станет нулевой за какое-либо значительное время. Это позволяет значительно снизить фрагментацию пакетов, поскольку теперь вместо того, чтобы иметь пакет для каждого запроса, есть хорошая вероятность, что в каждом пакете имеется несколько сообщений. Меньше пакетов обычно означает более высокую пропускную способность из-за уменьшения накладных расходов.

Ответ 2

Я знаю, что это решение было опубликовано пользователем, вероятно, более осведомленным, чем я, но, честно говоря, в вашем примере это ничего не делает. Настоящим убийцей в вашем последнем посте было то, что вы использовали List<> для фактического создания записей 10M в памяти до запуска цикла foreach в этой материализованной коллекции. Теперь вы используете IEnumerable<>, который не создает 10M одновременно в памяти, но один за другим (возможно, больше, если параллельно). Метод Batchify хорош... но если вы пропустите его, он должен работать точно так же. Лучший случай, это микро-оптимизация.