Да, я заметил, но при желании легко исправить на полную сортировку, нужно переделать функцию Sort(__m128i a[9]). Там сейчас 19 сравнений. Пишут, что для полной сортировки 9 элементов нужно 25, схема есть: https://www.sciencedirect.com/science/article/pii/S0022000015001397 Как раз преимущество подхода в том, что легко переделать на разные сортировочные сети, не придумывая каждый раз схему перемещения элементов в xmm-регистрах.
Ещё в медианном фильтре картинок этот алгоритм применяется: https://habr.com/ru/articles/204682/ там кстати код попроще выглядит, без перетасовок в xmm-регистрах. Используется векторизация в направлении "по массиву" (пикселей) - алгоритм абсолютно аналогичен скалярному, только вместо одного значения работаем с пачками по 16-32 элементов. Поэтому получается всего 6 интринсиков, можно даже и без них, у меня где-то был автовекторизованный вариант. Но да, это подходит для пикселей, а у вас большой пачки из сортируемых фрагментов может и не быть в наличии.
Новые (другие) типы. Также, использовать новую математику, интринсики и т.д.
Какие конкретно? Про SSE я знаю и объяснил, почему это не сработало.
И речь шла о том, что некоторые ускоряют код вообще без изменений. Именно это я и называю "прогресс в компиляторе". Либо сильно ускоряют при умеренных изменениях (затачивание под векторизацию). Единственное, что в Дельфи можно придумать с умеренными изменениями, это распараллеливание циклов. Но если очень захотеть, то можно их параллелить и в D5, хотя это и неудобно без анонимок. Инлайн работает криво и довольно часто замедляет вместо ускорения. И кардтрейсер у меня тоже замедлил.
Компилятор Делфи не застрял в 2000-х, вы заблуждаетесь.
У меня есть проект, который собирается разными версиями Дельфи начиная с D5/1999, недавно протестировал его на D12/2023. И какая же разница в скорости исполняемого кода за 24 года? Никакой! Ну может 1%, на уровне погрешности. Потому что там в основном целочисленные расчёты, и единственное заметное улучшение компилятора (плавающая точка на SSE в x64) не влияет. Так вот и получается, что для моих целей прогресса в компиляторе нет, и он застрял даже можно сказать в 90-х. Андроиды и прочие платформы не особо интересны, разве что Линукс... впрочем, по сообщениям там ещё хуже со скоростью, потому что это криво (с отключенной оптимизацией) прикрученный LLVM. Кто-то может проверить кардтрейсер на Дельфи/Линуксе?
На Си у меня нет проекта в точности аналогичного дельфийскому, но тот же кардтрейсер на gcc версий 4.9 и 12 (между ними 8 лет) показывает на разных ф-ях ускорение 10-20%, а иногда 2+ раза, если новая версия векторизовала, а старая нет. С компилятором 1999 года разница должна быть ещё больше хотя бы из-за поддерживаемого набора инструкций.
Ну что касается "написать правильно", то например C++ можно относительно небольшими правками разогнать в разы, я про это целую статью писал: https://habr.com/ru/articles/685228/ Дельфи от этих правок ускоряется гораздо меньше, потому что векторизацию компилятор не умеет совсем. Хотите SIMD - только хардкор, только ассемблерные простыни вручную.
включая C#, который "всего" в 2 раза медленнее нативного Delphi
Навскидку на этом: https://github.com/Mark-Kovalyov/CardRaytracerBenchmark получаю С# x64 - 9.4, Delphi x64 - 16.8 секунды (однопоток, консольный вывод в Дельфи отключен). Меньше - лучше, то есть С# быстрее почти в 2 раза. Почему? Ну, JIT C# основан вероятно на C++, а кодогенерация в плюсовых компиляторах со временем улучшается. У Дельфи она застряла на уровне 2000-х по причине неустроенности (перепродажи) и нехватки ресурсов на разработку. Я понимаю, почему вы держитесь за написанные и отлаженные млн. строк, просто слегка просвещаю насчёт современного соотношения сил.
Notepad++ даёт небольшую задержку при запуске. Да, едва заметную, может полсекунды, но разница с AkelPad чувствуется. Дело вкуса, но мне хотелось бы мгновенной реакции от контекстного вьюера всякой txt-мелочи.
Тоже когда-то пользовался Bred, потом перешёл на AkelPad - всё то же самое + подсветка, 600 кб. Сейчас проверил новые версии - уже и автодополнение есть, и сворачивание кода при макс. размере 4.5 мб. Но это плагины, при желании можно пользоваться как блокнотом.
64 Гб/c у Pci-Ex 4 - это суммарная в обе стороны, в одну сторону 32 Гб/c. По моим тестам реальная скорость в оптимальном для OpenCL варианте (pinned memory) около 75% от максимальной, 23-24 Гб/c на GF3080. Возможно, серией асинхронных запросов с разными картинками можно выжать больше, у меня гонялась одна.
Тесты с обработкой я тоже делал, и получалось, что единичные простые операции (напр. gauss blur) всё-таки нет смысла гонять в видеокарту. Чем сложнее, тем лучше: AHD debayer - умеренное ускорение раза в два, нейросети - да, самое то :) Или переносить на GPU весь конвейер из простых операций. При этом отлаживать и поддерживать GPU-код сложнее.
"реализация GetEvensCount без if всё-таки чуть-чуть медленнее, чем с if'ами"
Хотя это и несколько оффтоп, но попиарю в очередной раз компилятор Clang: https://gcc.godbolt.org/z/WoMbrx8zx SIMD, развёртка, считаем 32 числа за итерацию - эта версия уж точно не медленнее. Значит, в общем случае избавляться от if выгоднее.
Только я вставлял в сишный проект, поэтому переправил всё с плюсов на чистый Си. Но вряд ли это влияет. И я тоже ожидал, что будет мухлевать с итерациями, но нет, время явно зависит от Cycles.
код
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
unsigned long sum_array(unsigned long* array, unsigned long size)
{
unsigned long sum = 0;
for (unsigned long i = 0; i < size; ++i)
sum += array[i];
return sum;
}
int main()
{
const unsigned long n = 4096;
const Cycles = 2000000;
const Cycles2 = 20;
unsigned long array[n];
srand(42);
for (unsigned long i = 0; i < n; ++i)
array[i] = rand();
LARGE_INTEGER frequency;
LARGE_INTEGER t1, t2, t3, t4;
QueryPerformanceFrequency(&frequency);
unsigned long s = 0;
double minTime = 100000000;
for (int m = 0; m < Cycles2; m++) {
QueryPerformanceCounter(&t1);
for (int i = 0; i < Cycles; i++) {
s = sum_array(array, n);
}
QueryPerformanceCounter(&t2);
double elapsedTime=(double)(t2.QuadPart-t1.QuadPart)/frequency.QuadPart;
if (elapsedTime < minTime) minTime = elapsedTime;
}
printf("sum: %lu\n", s);
printf("microseconds: %.5f seconds\n", minTime);
return 0;
}
Мне удалось выжать почти 2 раза (1.8), но это с минимальным размером массива (4096) и с двумя внешними циклами, один просто крутит 2000000 итераций, другой выбирает минимальное время из 20 попыток. Clang 14 / gcc 12. В реальных условиях наверное чаще будет 5%, чем 2 раза.
Вы же там ниже пишете, что в память упирается - ну правильно, 40 мб. Автор статьи специально на небольшом размере тестировал, чтобы помещалось в самый быстрый кэш. Можно гонять тест коротких данных много раз.
Получается, в Skylake уже 3 модуля для векторного сложения (Int Vect ALU), так что Clang где-то и прав. Я брал информацию по вычислительным модулям у Агнера Фога, может он намеренно упоминал только "настоящие" модули с умножением и FMA: https://www.agner.org/optimize/blog/read.php?i=838
Clang не согласен, он делает ещё развёртку SIMD-кода. https://gcc.godbolt.org/z/3M8W81rMv Можно добавить к статье, что сказанное про множественные скалярные арифметические модули справедливо и для SIMD, у Intel сейчас 2 * 256 бит, у AMD 4 * 128 бит. Получается, развёртка Clang 4 * 256 избыточна, ну вот такая у него "широкая душа", любит развернуть с настройками по умолчанию. Может, в расчёте на будущие процессоры.
Надо было уточнить - мой опыт относится к Си/Дельфи, Go толком не знаю. Сейчас попытался переписать функцию на Go: https://gcc.godbolt.org/z/ernMd4qE3 Без проверок диапазонов ассемблер похож, кстати, на Дельфи - векторизации нет, но в целом достаточно чистый код. И так или иначе он работает гораздо быстрее ввода-вывода png. Я даже проверил: Загрузка 24-битного png через GDI+: 22 мс Извлечение канала на Дельфи (имитируем Go): 0.76 мс Извлечение канала на Си: 0.19 мс Отсюда и вывод, что не должно копирование байтиков заметно тормозить... не знаю, может автору тоже отключить проверки глобально директивой B-? Или это плохой тон?
В Си простые циклы векторизуются автоматически, интринсики вспоминать не надо. https://gcc.godbolt.org/z/a9ecW4hx4 Для SIMD конечно лучше, если в исходнике 4 байта на пиксель, меньше команд понадобится (или менее "тяжёлых").
Но по моему опыту, копирование с извлечением синего канала не должно сильно влиять. В основном должно упираться в распаковку/упаковку png, потому что png вообще медленный. Как я слышал, zlib-овский inflate/deflate слабо (или вообще никак) ускоряется через SIMD. У вас получилось 20% на выборе оптимизированной библиотеки - ну вот примерно так, да, и это мало. Качественные библиотеки jpeg ускоряют в разы.
Поэтому главное - грамотное распараллеливание упаковки/распаковки. И у вас хороший результат, я не ожидал, что можно делать чтение 1000 картинок и упаковку одной огромной за 0.23 c.
Да, я заметил, но при желании легко исправить на полную сортировку, нужно переделать функцию Sort(__m128i a[9]).
Там сейчас 19 сравнений. Пишут, что для полной сортировки 9 элементов нужно 25, схема есть:
https://www.sciencedirect.com/science/article/pii/S0022000015001397
Как раз преимущество подхода в том, что легко переделать на разные сортировочные сети, не придумывая каждый раз схему перемещения элементов в xmm-регистрах.
Ещё в медианном фильтре картинок этот алгоритм применяется:
https://habr.com/ru/articles/204682/
там кстати код попроще выглядит, без перетасовок в xmm-регистрах. Используется векторизация в направлении "по массиву" (пикселей) - алгоритм абсолютно аналогичен скалярному, только вместо одного значения работаем с пачками по 16-32 элементов. Поэтому получается всего 6 интринсиков, можно даже и без них, у меня где-то был автовекторизованный вариант.
Но да, это подходит для пикселей, а у вас большой пачки из сортируемых фрагментов может и не быть в наличии.
Покажу как я прикручивал инлайн к кардтрейсеру. И конечно проверял - да, заинлайнилось, но медленнее на 2 секунды.
И что не так?
Какие конкретно? Про SSE я знаю и объяснил, почему это не сработало.
И речь шла о том, что некоторые ускоряют код вообще без изменений. Именно это я и называю "прогресс в компиляторе". Либо сильно ускоряют при умеренных изменениях (затачивание под векторизацию).
Единственное, что в Дельфи можно придумать с умеренными изменениями, это распараллеливание циклов. Но если очень захотеть, то можно их параллелить и в D5, хотя это и неудобно без анонимок.
Инлайн работает криво и довольно часто замедляет вместо ускорения. И кардтрейсер у меня тоже замедлил.
У меня есть проект, который собирается разными версиями Дельфи начиная с D5/1999, недавно протестировал его на D12/2023.
И какая же разница в скорости исполняемого кода за 24 года?
Никакой! Ну может 1%, на уровне погрешности.
Потому что там в основном целочисленные расчёты, и единственное заметное улучшение компилятора (плавающая точка на SSE в x64) не влияет.
Так вот и получается, что для моих целей прогресса в компиляторе нет, и он застрял даже можно сказать в 90-х.
Андроиды и прочие платформы не особо интересны, разве что Линукс... впрочем, по сообщениям там ещё хуже со скоростью, потому что это криво (с отключенной оптимизацией) прикрученный LLVM. Кто-то может проверить кардтрейсер на Дельфи/Линуксе?
На Си у меня нет проекта в точности аналогичного дельфийскому, но тот же кардтрейсер на gcc версий 4.9 и 12 (между ними 8 лет) показывает на разных ф-ях ускорение 10-20%, а иногда 2+ раза, если новая версия векторизовала, а старая нет. С компилятором 1999 года разница должна быть ещё больше хотя бы из-за поддерживаемого набора инструкций.
Ну что касается "написать правильно", то например C++ можно относительно небольшими правками разогнать в разы, я про это целую статью писал:
https://habr.com/ru/articles/685228/
Дельфи от этих правок ускоряется гораздо меньше, потому что векторизацию компилятор не умеет совсем. Хотите SIMD - только хардкор, только ассемблерные простыни вручную.
Навскидку на этом:
https://github.com/Mark-Kovalyov/CardRaytracerBenchmark
получаю С# x64 - 9.4, Delphi x64 - 16.8 секунды (однопоток, консольный вывод в Дельфи отключен). Меньше - лучше, то есть С# быстрее почти в 2 раза.
Почему? Ну, JIT C# основан вероятно на C++, а кодогенерация в плюсовых компиляторах со временем улучшается. У Дельфи она застряла на уровне 2000-х по причине неустроенности (перепродажи) и нехватки ресурсов на разработку.
Я понимаю, почему вы держитесь за написанные и отлаженные млн. строк, просто слегка просвещаю насчёт современного соотношения сил.
Notepad++ даёт небольшую задержку при запуске. Да, едва заметную, может полсекунды, но разница с AkelPad чувствуется. Дело вкуса, но мне хотелось бы мгновенной реакции от контекстного вьюера всякой txt-мелочи.
Тоже когда-то пользовался Bred, потом перешёл на AkelPad - всё то же самое + подсветка, 600 кб.
Сейчас проверил новые версии - уже и автодополнение есть, и сворачивание кода при макс. размере 4.5 мб. Но это плагины, при желании можно пользоваться как блокнотом.
64 Гб/c у Pci-Ex 4 - это суммарная в обе стороны, в одну сторону 32 Гб/c. По моим тестам реальная скорость в оптимальном для OpenCL варианте (pinned memory) около 75% от максимальной, 23-24 Гб/c на GF3080. Возможно, серией асинхронных запросов с разными картинками можно выжать больше, у меня гонялась одна.
Тесты с обработкой я тоже делал, и получалось, что единичные простые операции (напр. gauss blur) всё-таки нет смысла гонять в видеокарту. Чем сложнее, тем лучше: AHD debayer - умеренное ускорение раза в два, нейросети - да, самое то :)
Или переносить на GPU весь конвейер из простых операций. При этом отлаживать и поддерживать GPU-код сложнее.
Советы компиляторы уже дают, в godbolt для Clang есть окно Optimization (через кнопку +).
Насчёт loop distribution он оказался прав, но сам не осилил, пришлось вручную делать.
( https://habr.com/ru/articles/685228/ )
"реализация GetEvensCount без if всё-таки чуть-чуть медленнее, чем с if'ами"
Хотя это и несколько оффтоп, но попиарю в очередной раз компилятор Clang:
https://gcc.godbolt.org/z/WoMbrx8zx
SIMD, развёртка, считаем 32 числа за итерацию - эта версия уж точно не медленнее.
Значит, в общем случае избавляться от if выгоднее.
Только я вставлял в сишный проект, поэтому переправил всё с плюсов на чистый Си. Но вряд ли это влияет. И я тоже ожидал, что будет мухлевать с итерациями, но нет, время явно зависит от Cycles.
код
Мне удалось выжать почти 2 раза (1.8), но это с минимальным размером массива (4096) и с двумя внешними циклами, один просто крутит 2000000 итераций, другой выбирает минимальное время из 20 попыток. Clang 14 / gcc 12.
В реальных условиях наверное чаще будет 5%, чем 2 раза.
Вы же там ниже пишете, что в память упирается - ну правильно, 40 мб.
Автор статьи специально на небольшом размере тестировал, чтобы помещалось в самый быстрый кэш. Можно гонять тест коротких данных много раз.
Получается, в Skylake уже 3 модуля для векторного сложения (Int Vect ALU), так что Clang где-то и прав.
Я брал информацию по вычислительным модулям у Агнера Фога, может он намеренно упоминал только "настоящие" модули с умножением и FMA:
https://www.agner.org/optimize/blog/read.php?i=838
Clang не согласен, он делает ещё развёртку SIMD-кода.
https://gcc.godbolt.org/z/3M8W81rMv
Можно добавить к статье, что сказанное про множественные скалярные арифметические модули справедливо и для SIMD, у Intel сейчас 2 * 256 бит, у AMD 4 * 128 бит. Получается, развёртка Clang 4 * 256 избыточна, ну вот такая у него "широкая душа", любит развернуть с настройками по умолчанию. Может, в расчёте на будущие процессоры.
Не на том материале рекламируете :)
Naked to the bone
Надо было уточнить - мой опыт относится к Си/Дельфи, Go толком не знаю. Сейчас попытался переписать функцию на Go:
https://gcc.godbolt.org/z/ernMd4qE3
Без проверок диапазонов ассемблер похож, кстати, на Дельфи - векторизации нет, но в целом достаточно чистый код.
И так или иначе он работает гораздо быстрее ввода-вывода png. Я даже проверил:
Загрузка 24-битного png через GDI+: 22 мс
Извлечение канала на Дельфи (имитируем Go): 0.76 мс
Извлечение канала на Си: 0.19 мс
Отсюда и вывод, что не должно копирование байтиков заметно тормозить... не знаю, может автору тоже отключить проверки глобально директивой B-? Или это плохой тон?
В Си простые циклы векторизуются автоматически, интринсики вспоминать не надо.
https://gcc.godbolt.org/z/a9ecW4hx4
Для SIMD конечно лучше, если в исходнике 4 байта на пиксель, меньше команд понадобится (или менее "тяжёлых").
Но по моему опыту, копирование с извлечением синего канала не должно сильно влиять. В основном должно упираться в распаковку/упаковку png, потому что png вообще медленный. Как я слышал, zlib-овский inflate/deflate слабо (или вообще никак) ускоряется через SIMD. У вас получилось 20% на выборе оптимизированной библиотеки - ну вот примерно так, да, и это мало. Качественные библиотеки jpeg ускоряют в разы.
Поэтому главное - грамотное распараллеливание упаковки/распаковки. И у вас хороший результат, я не ожидал, что можно делать чтение 1000 картинок и упаковку одной огромной за 0.23 c.