Скоростная обработка тиков. Рекомендации
Последнее время приходится много работать с тиковыми данными и, естественно, приходится их обрабатывать в TSLab. Так как свечей много и в каждой свече еще больше тиков, то процесс обработки оных может занимать длительное время. Очень хочется чтобы работало все быстрее и как итог, было решено протестировать основные паттерны работы с тиками. В результате сделаем вывод о том, как реализуется скоростная обработка тиков чтобы было быстро и хорошо. И да, картинок не будет 🙂 .
Скоростная обработка тиков. Постановка задачи.
Схема обработки тиков весьма однообразна и содержит пару базовых паттернов.
— прямой обсчет тиков текущей свечи. Мы работаем прямо с тиками без создание промежуточных списков.
— обсчет тиков свечи через промежуточные коллекции и списки.
Вообще, вся схема обычно происходит путем перебора всех свечей начиная с первой и кончая последней. На каждом баре мы что-то вычисляем, и потом результат куда-то выдаем. В тестовом скрипте роль бара будет выполнять массив с числами double (якобы тики), а процесс перебора баров реализуем через простой цикл.
Сразу оговорюсь, что стоит учесть возможную разницу в скорости работы с коротким и длинным массивом, поэтому тестовый скрипт будет комбинировать длину массива и выдавать итоги для каждой длины. При работе с объектами, будут еще дополнительные задержки на обращение к данным, но пока это мы не будем моделировать.
Для измерения времени работы, будет использовать специальный класс System.Diagnostics.Stopwatch. Он позволит измерить время с высоким уровнем точности. Каждый проход по свечам будем замерять как единую операцию. В итоге получим список измерений для разной длины массива тиков.
Обработка коллекций может происходить с использованием трех типичных инструментов:
— стандартные циклы (for, while, do)
— цикл foreach
— linq команды
Поэтому в тесте будет разбирать производительность каждой из трех, чтобы понять какой вариант нам лучше использовать.
Тестовый скрипт
Скрипт получился не очень большой но покрывающий все наши исходные задачи.
// чтобы набор чисел всегда был одинаков, используем seed var rnd = new Random(100); // стартовый размер массива данных и делитель var arrSize = 100000000; var div = 1; var currSize = arrSize; while (currSize > 10) { div *= 10; currSize = arrSize / div; // якобы массив тиков var arr = new double[currSize]; for (int i = 0; i < arr.Length; i++) arr[i] = rnd.Next(1, 1000); var watch = new Stopwatch(); watch.Start(); var count = 0.0; // якобы перебор свечек for (var i = 0; i < div; i++) { count = 0; // сюда будем вставлять команды работы с тиками } var msg = string.Format("ArrLength: {0}, Loop: {1}, Time: {2}, count: {3}", currSize, div, watch.Elapsed, count); Debug.WriteLine(msg); }
Прямой обсчет тиков
Прямой обсчет не использует промежуточных списков и массивов, а напрямую извлекает тики из массива тиков и производит арифметические операции. Примеры такой логики:
- подсчет числа сделок на покупку в свече.
- расчет объема свечи из тиковых сделок.
- подсчет объема всех покупок в свече.
- подсчет сделок больше заданного объема в свече.
В общем, примеры показывают направление нашей мысли. Для теста будем рассчитывать объем всех сделок с объемом больше 500. Максимальный объем тика у нас 1000, как следует из тестового скрипта.
Примеры кода для каждого варианта:
// linq count = arr.Sum(v => v > 500 ? v: 0); // for for (int k = 0; k < arr.Length; k++) count += arr[k] > 500 ? arr[k] : 0; // foreach foreach (var item in arr) count += item > 500 ? item : 0;
Результаты следующие:
for |
ArrLength: 3000000, Loop: 100, Time: 00:00:03.9433100, count: 1122845420 ArrLength: 300000, Loop: 1000, Time: 00:00:03.9302639, count: 112374876 ArrLength: 30000, Loop: 10000, Time: 00:00:03.9892026, count: 11198670 ArrLength: 3000, Loop: 100000, Time: 00:00:03.8094046, count: 1111242 ArrLength: 300, Loop: 1000000, Time: 00:00:03.7044354, count: 116344 ArrLength: 30, Loop: 10000000, Time: 00:00:03.5904666, count: 12634 ArrLength: 3, Loop: 100000000, Time: 00:00:02.6527128, count: 2169 |
foreach |
ArrLength: 30000000, Loop: 10, Time: 00:00:04.2863756, count: 11237564998 ArrLength: 3000000, Loop: 100, Time: 00:00:04.2342477, count: 1122845420 ArrLength: 300000, Loop: 1000, Time: 00:00:04.2351377, count: 112374876 ArrLength: 30000, Loop: 10000, Time: 00:00:04.1641656, count: 11198670 ArrLength: 3000, Loop: 100000, Time: 00:00:03.7539018, count: 1111242 ArrLength: 300, Loop: 1000000, Time: 00:00:03.6106301, count: 116344 ArrLength: 30, Loop: 10000000, Time: 00:00:03.3811633, count: 12634 ArrLength: 3, Loop: 100000000, Time: 00:00:02.4263865, count: 2169 |
linq |
ArrLength: 30000000, Loop: 10, Time: 00:00:10.2037127, count: 11237564998 ArrLength: 3000000, Loop: 100, Time: 00:00:10.1790545, count: 1122845420 ArrLength: 300000, Loop: 1000, Time: 00:00:10.6790103, count: 112374876 ArrLength: 30000, Loop: 10000, Time: 00:00:10.1260764, count: 11198670 ArrLength: 3000, Loop: 100000, Time: 00:00:10.1368777, count: 1111242 ArrLength: 300, Loop: 1000000, Time: 00:00:09.6797452, count: 116344 ArrLength: 30, Loop: 10000000, Time: 00:00:09.5827869, count: 12634 ArrLength: 3, Loop: 100000000, Time: 00:00:16.0744673, count: 2169 |
Как видно, linq остает почти в 3 раза от других вариантов цикла. При этом у linq какое-то неадекватное увеличение времени выполнения при размере массива 3. Это не ошибка, результат повторяется стабильно. Видимо накладные расходы на использование linq здесь вылезают по полной программе и время резко увеличивается. Так что, хоть и красиво и удобно, но для обработки тиков linq не подходит при прямой работе с тиками. Увы и ах.
for и foreach идут ноздря в ноздрю и между ними выбирать не стоит. Какой вариант удобнее такой и используйте, но если вы патологический кодер, то используйте всегда базовые циклы, ведь они немного быстрее 🙂 .
Обсчет тиков через промежуточные коллекции.
Данный паттерн встречается не менее часто чем предыдущий. Примеры реализации:
- график footprint который можно увидеть в TSLab
- неоднократное обращение к определенной выборке тиков, например к тикам покупок.
- любой другой случай где мы часть тиков помещаем в отдельную коллекцию.
Так как выше выяснили что foreach так же быстр как for, то не будем задействовать for в тестах.
Примеры кода для каждого варианта:
// linq без конвертации в другой вид коллекции var result = arr.Where(v => v > 500); // linq с корвертацией в массив var result = arr.Where(v => v > 500).ToArray(); // foreach с созданием отдельного списка var result = new List(); foreach (var item in arr) { if (item > 500) result.Add(item); }
После получения коллекции, будем производить с ней какую либо операцию, например, подсчет объема тиков.
// проводим операцию над коллекцией полученной выше одним из способов foreach (var item in result) count += item;
Результаты следующие:
foreach |
ArrLength: 10000000, Loop: 10, Time: 00:00:02.4438366, count: 3746495393 ArrLength: 1000000, Loop: 100, Time: 00:00:02.3367783, count: 374321244 ArrLength: 100000, Loop: 1000, Time: 00:00:02.4252638, count: 37297133 ArrLength: 10000, Loop: 10000, Time: 00:00:02.4478711, count: 3756152 ArrLength: 1000, Loop: 100000, Time: 00:00:01.4827645, count: 355438 ArrLength: 100, Loop: 1000000, Time: 00:00:01.7289573, count: 39551 ArrLength: 10, Loop: 10000000, Time: 00:00:01.7987754, count: 2111 |
linq |
ArrLength: 10000000, Loop: 10, Time: 00:00:02.9777427, res.count: 4995004 ArrLength: 1000000, Loop: 100, Time: 00:00:02.9503232, res.count: 499069 ArrLength: 100000, Loop: 1000, Time: 00:00:02.9179539, res.count: 49788 ArrLength: 10000, Loop: 10000, Time: 00:00:02.9169527, res.count: 5013 ArrLength: 1000, Loop: 100000, Time: 00:00:02.7192137, res.count: 478 ArrLength: 100, Loop: 1000000, Time: 00:00:02.5827635, res.count: 50 ArrLength: 10, Loop: 10000000, Time: 00:00:02.8403376, res.count: 3 |
linq array |
ArrLength: 10000000, Loop: 10, Time: 00:00:03.8770327, count: 3746495393 ArrLength: 1000000, Loop: 100, Time: 00:00:03.8029816, count: 374321244 ArrLength: 100000, Loop: 1000, Time: 00:00:03.8641673, count: 37297133 ArrLength: 10000, Loop: 10000, Time: 00:00:03.9113998, count: 3756152 ArrLength: 1000, Loop: 100000, Time: 00:00:03.2722251, count: 355438 ArrLength: 100, Loop: 1000000, Time: 00:00:03.9661614, count: 39551 ArrLength: 10, Loop: 10000000, Time: 00:00:04.2493606, count: 2111 |
Здесь, работа с linq себя неплохо зарекомендовала, но только если не переводить коллекцию в массив или список. Если будете конвертировать то сразу теряете в два раза производительность. Возможно, конвертация даст плюс если нужно много раз обратиться к коллекции, тогда мы избавимся от накладных расходов linq но получим замедление на создание массива. В общем – не лучший вариант.
В лидерах у нас создание коллекции. Память, конечно, это будет жрать, но чаще всего скорость важнее. Поэтому работаем через стандартные циклы и через создание списка или массива.
Тяжело в учении – легко в бою
В завершение, хотелось проверить как будет работать скоростная обработка тиков в реальных условиях, то есть, в TSLab. Тиковых данных у меня достаточно, засим переходим в реальные полевые условия (можно накачать тики с http://ftp.moex.ru или достать где-то еще). Тестировать буду на простом примере из библиотеки индикаторов RusAlgo. По умолчанию, там используется linq в виду его красоты и удобства написания. Кубик расчета объема всех сделок на покупку и продажу, подойдет как нельзя кстати. Создадим новую версию кубика, с применением стандартных циклов и будем сравнивать скорость обработки всех баров. Для того чтобы время было ощутимое, в коде скрипта завернем цикл, в котором будем многократно повторять обсчет тиков, что даст нам более адекватные цифры для сравнения.
Важное замечание: нужно один раз прогнать скрипт, чтобы тики подрузились с диска, тогда время обсчета будет зависеть только от скорости скрипта, а не от скорости вашего диска.
Приводить код тестового скрипта не буду, сразу переходим к итогам:
13:39:49.98 : Direct: for: 00:00:14.5599726 |
13:40:13.65 : Direct: linq: 00:00:23.6646459 |
Как видим, linq оказался в 2 раза тормознее (ну не совсем в 2, но разница серьезная). Ранее, мы получали еще большую разницу. Почему? Очевидно, что работа с объектами сделок и запрос свойств объектов, дает затраты и эти затраты одинаковы в обоих случаях, но сильно влияют на общую скорость работы.
Давайте для окончательной проверки, возьмем кубик суммирующий объемы всех сделок на покупку/продажу. Его особенность в том, что linq используется правильно, а именно в виде связки
value = trades.Where(trd => trd.Direction == Direction).Sum(trd => trd.Quantity);
Такая связка позволяет избежать создания промежуточных списков и отработать достаточно быстро и удобно.
Посмотрим что у нас получится для обоих вариантов:
13:53:32.71 : Direct: linq: 00:00:24.4801047 |
13:53:08.23 : Direct: for: 00:00:17.3492000 |
Разрыв стал меньше, но все равно существенный. Если скрипт работает 17 минут вместо 24-х, это заметно.
Выводы
Статья была написана не с целью написания, а в процессе проведения тестов и оптимизации своих кодов работы с тиками. Это отразилось и на сборке индикаторов RusAlgo, в виде ускорения работы некоторых индикаторов. Ну, и память не вечна, лучше всегда иметь шпаргалку на будущее 🙂 .
Собственно итоги в куче:
- Везде где можно используем стандартные циклы (for,while,do) или foreach. Ускорение до двух раз.
- Если нужна промежуточная коллекция, лучше создать ее в виде списка или массива чем использовать linq. Памяти сожрет больше, скорость будет тоже больше. Ускорение небольшое, но есть, особенно если запрос к коллекции будет не один а много.
- Скоростная обработка тиков это не миф, а реальность 🙂 .
PS: Всем добра. И не стоит заниматься оптимизацией скорости, если не возникла проблема выполнения скриптов в течение минут десяти и более 🙂 .
Родион Скуратовский - ra81
Можно записаться на следующий поток ОнЛайн курса, информацию по которому можно посмотреть тут:
Не откладывайте свой шанс заработать на бирже уже сегодня!
Читайте также:
- Январь 2021
- Декабрь 2020
- Ноябрь 2020
- Октябрь 2020
- Сентябрь 2020
- Август 2020
- Июль 2020
- Июнь 2020
- Май 2020
- Апрель 2020
- Март 2020
- Февраль 2020
- Январь 2020
- Декабрь 2019
- Ноябрь 2019
- Октябрь 2019
- Сентябрь 2019
- Август 2019
- Июль 2019
- Июнь 2019
- Май 2019
- Апрель 2019
- Март 2019
- Февраль 2019
- Январь 2019
- Декабрь 2018
- Ноябрь 2018
- Октябрь 2018
- Сентябрь 2018
- Август 2018
- Июль 2018
- Июнь 2018
- Май 2018
- Апрель 2018
- Март 2018
- Февраль 2018
- Январь 2018
- Декабрь 2017
- Ноябрь 2017
- Октябрь 2017
- Сентябрь 2017
- Август 2017
- Июнь 2017
- Май 2017
- Апрель 2017
- Март 2017
- Февраль 2017
- Январь 2017
- Декабрь 2016
- Ноябрь 2016
- Октябрь 2016
- Сентябрь 2016
- Август 2016
- Июль 2016
- Июнь 2016
- Май 2016
- Апрель 2016
- Март 2016
- Февраль 2016
- Январь 2016
- Декабрь 2015
- Ноябрь 2015
- Октябрь 2015
- Сентябрь 2015
- Февраль 2015
- Январь 2015
- Август 2014