Полна чудес могучая природа

А в SSE4.1 оказывается есть минимум-максимум для 16-битных целых. 8 штук зараз.

Хреначит 11 гигабайт в секунду, сдается мне что параллелить по ядрам дальше смысла нет, упрешься в память.

И dot-product есть, правда только для float/double.

Чешутся руки забить на владельцев AMD и всего младше Penryn, держите меня...

Comments

Только вот в АМД-шных пока что нет того sse4.1.
Так что не факт, что в Фотошопе 6 это будет использовано.
да и зачем ? 5-й и так быстр, они наконец-то (если верить написанному на форумах) всё вычислительное переписали с использованием sse2, а не только отдельные фильтры.

А причем тут фотошоп?

Я чисто о своем (см. теги). Смотрю на хотспоты, смотрю на SSE-инструкции и думаю.

Я, конечно, эти тестовые примеры еще и в OpenMP заверну, может оказаться, что на 4 горшках оно и без SSE упирается в память, тогда и ладно.

пардон 8-)
А это сильно ускорит ?
Ну, если можно получить ту же скорость на 2-х ядрах, а не на 4-х, то было бы классно.

Минимум-максимум - ровно вчетверо быстрее. На одном треде.

Умножение вектора на матрицу еще не попробовал, но сдается мне что разница будет еще больше: тот код что сейчас есть офигачивает ~300Mb/sec, а причин не упереться в ту же память (т.е. в ~5.5Gb/sec т.к. мы еще и пишем туда) я не вижу.

Ого.
Вопрос в том, насколько это ускорит всё преобразование, там же и другие вычисления.

>> тот код что сейчас есть офигачивает ~300Mb/sec,

Те. 300 метров raw данных в секунду ? А на каком преобразовании ?

Да, там конечно другие вычисления и та же интерполяция - относительно медленная.

Но 600 миллисекунд там, 250 - сям, глядишь и еще секунду снимем с 20-мегапиксельной картинки. Если без демозаики - а этот режим весьма интересен для всяких просмотрщиков - то я надеюсь в 1.5 секунды для 20-мегапикселей уложиться на все. Это для Кэнона, который медленно распаковывается, с теми же Сонями нет причин не уложиться в полсекунды.

А 300Mb/sec - это цветовое преобразование, на входе тройка R-G-B (или четверка R-G-B-G) и матрица матричного профиля, на выходе - R-G-B в каком-то приличном цветовом пространстве вроде sRGB/gamma 1

Кэнон чем-то ресурсоёмким запакован ?

Ну да. Lossles jpeg, да еще и с нетрадиционным layout, только что обсуждали.

И одним куском, DNG с такой же паковкой - побит на tiles (необязательно, но по факту - очень часто) и можно параллелить по процессорам хотя бы.

>> т.е. в ~5.5Gb/sec <<
Это откуда такие данные ?!?
и что это за память такая ?

(или это в битах и для кэша? :-))

Да в-принципе да, может быть и больше можно.

Всякие тесты memory-copy намеривают для DDR3-1600 (трехканальной) в районе 8-9 гигабайт в секунду, это и есть целевое значение.

"8-9 гигабайт в секунду"

это же наверное на одном ядре.. чтобы всю псп забить, насколько я помню, нужно всеми ядрами пыхтеть..

Ну формально - 3 канала на ~11gb/sec (DDR3, 1500 гигагерцiв), итого 33, это на чтение. Как прикинуть влияние задержек - понятия не имею.

А по факту - вот сижу и щупаю, сколько можно нагадить в память зараз.

В-общем, один movntps/movntdq льет примерно 5 гигабайт/сек.

Он же с openmp - 6-6.1 гигабайт/сек. Если зажать число потоков до 4 (у меня 8 ядер: 4 да гипертрединг), то 6.7
Как-то я ожидал большего, если честно. Хотя все равно приятно.

Код, который пишет компилятор - медленнее, примерно 5gb/sec в пике.

что-то маловато.. я думал если захотеть, то на core i7 можно достичь больше 20GiB/s на чтение и запись..

Может там кто-то по рукам кнутом бьёт?
Например можно попробовать два раза тест прогнать, замеряя только время второго (может calloc, но лучше всё таки один разу "вручную" заполнить).. Может там такой оверхед от ОСи - например windows реально выделяет память только под реально используемые страницы, или ещё какой кнут..

Ну вот результат такой уже:

1) Заполнение массива (1.5 гигабайта) через movntps - ~4.5Gb/sec без OpenMP и 5.8 - в 8 потоков.

2) операция вида
считать 4 float
три dot-product и два blendps
записать 4 float туда же, куда писали
4.8 Gb/sec на одном ядре и до 11.2 - на OpenMP (числом тредов не управляю).
В том смысле, что 100 мегапикселей (по 16 байт на пиксель) за 313 миллисекунд.

Возможно, действительно на 1-м шаге мне выделяют память по страничкам, лениво так. Сейчас добавлю второй проход для шага 1.

Агабля!

Второй проход той же инициализациями (но другими значениями, чтобы было видно что работает) - 12.3Gb/sec в один поток и 16.7 - во всю дурь.

Во, теперь похоже на правду совсем.

О круто!
Вот только всё равно хочу больше 20GiB/s, причём на своих 3xDDR3-1333 :)

Кстати, думаю не обязательно прям все значения предварительно инициализировать - можно "прогуляться" с шагом 4KiB, или что-то типа этого.. А может есть что-то ОС-зависимое, но хочется без этого..

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

Вот только всё равно хочу больше 20GiB/s, причём на своих 3xDDR3-1333 :)

значит vs2010, core i7 930, 3xDDR3-1333, ht выключен, openmp 4 потока, размер массива - 800MiB - выровнен по 64 байтам, каждый поток пишет/читает по 4 xmm(то есть 64 байта), перед началом всё инициализируется, основной цикл повторяется 16 раз

_mm_stream_pd компилируется в movntpd ~ 17Gb/s
_mm_store_pd компилируется в movapd ~ 12Gb/s
_mm_load_p компилируется в movapd ~ 21.7Gb/s

load_pd - это же чтение такое?

Ну да, с префетчем и если регистров хватает, наверное как-то так. 17gb/sec.

Чистое копирование меня интересует постольку-поскольку, хотя вот вместе с конверсией типа, скажем float в short - интересно будет попробовать.

да, load_pd это чтение..
Я видел в тестах памяти SiSoftware Sandra псп памяти около 22Gb/s, и так как запись не дотягивает и до 20, решил проверить и чтение.. В итоге, в тесте Sandra, скорей всего проверяется чтение..
Мне вот интересно, почему у вас на DDR3-1600 примерно такие же результаты по записи, может нужно в биосе частоту памяти установить? Например у меня в биосе явно стоит 1333, а не авто, я по-моему это явно устанавливал.

У меня не 1600, а 1500 реально. Во всяком случае, оно так пишет про себя при загрузке.

Но, возможно, какие-то другие клоки при этом похуже.

Поднял частоту памяти (и, заодно, процессора), процессор стал 3.2, память - 1600.

Запись выросла до 19.2GB/sec (_mm_stream_ps)

Блин, новые горшки объявили, которые Sandy Bridge. Всего в два раза больше (в смысле AVX), но памяти - только два канала. То бишь вместо 25Gb/sec полосы - только 17 намеривают.

К концу года обещают 4-канальные, конечно, но ведь столько ждать - это утомиться!

Как-то SSE, в смысле уже AVX, совсем медленно развивается. Через десять лет после SSE2, они только созрели удвоить размер регистров..

А далеко не всем алгоритмам от удвоения регистров так уж полегчает. Скажем, тот же imaging - в вектор влезал пиксель (R,G,B и альфа-канал) - и с пикселем удобно работать. А два пикселя - нафига?

А во внутренностях, при сохранении набора команд, работа постоянно шла: два (теперь вот уже три) порта исполнения, все такое. Регистровые файлы росли, опять же.

Мне вот гораздо больше нравится, что у новых горшков можно (вроде бы! пока всерьез не изучал) о выравнивании гораздо меньше заботиться. То есть старые куски кода, которые грузили unaligned данные - заработают быстрее, даже если их вообще не трогать и не перекомпилировать.

А далеко не всем алгоритмам от удвоения регистров так уж полегчает

Не, ну понятно. Просто например в double векторизовать не всегда очень выгодно (при sse2) - лезть в ассемблер или intrinsics, только из-за двух-кратного ускорения (также учитывая что точность теряется, так как fpu это всё-таки 80бит..), не очень хочется.
Ладно если float..

Мне вот гораздо больше нравится, что у новых горшков можно (вроде бы! пока всерьез не изучал) о выравнивании гораздо меньше заботиться

А вот это кстати по-моему ещё с Core I7 (в смысле не в новейших). Я по-моему даже какой-то тест делал - на Core2 unaligned инструкция чуть ли не в два раза медленней чем aligned, когда на Core I7 скорость точно такая же..

Да, вы кругом правы. И про double (про него не думал, мне сейчас не надо) и про unaligned load

Вот кстати gpu в этом плане более православные..
Вот на CPU возможно идёт некий счётный процесс, который грузит все ядра, причём кэши и регистры у него забиты впритык (например тот же gemm), мало того что из-за вклинивания всяких системных и не очень процессов регистры могут тасоваться туда-сюда, так и ещё какая-нибудь сволочь может взять да и сбросить/сожрать весь кэш.. (можно конечно отдавать ядра отдельным процессам, но всё же дёготь есть)
То есть я к тому, что концепция обособленного вычислятора(будь то gpu или larabee), достаточно не плоха. А самое главное - программисты всегда будут сыты ;)

Концепция отличная и, по всей видимости, для интересных мне приложений (с картинками) будет сильно производительнее на каком-то современном железе.

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

А с OpenCL/CUDA - нужно довольно большие куски переписывать. Более объемная задача.

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

Какой профайлер используете? VTune?

Под виндами - VTune, на маке - Shark. Но на маке редко теперь работаю.

Кстати, там же ещё есть turbo boost, так что можно попробовать OpenMP c num_threads(2)

Ну вот получилось писать на 6.7Gb/sec, правда из четырех тредов. Из одного только пять.

О боже!
вот уж воистину - ignorance is bliss

это ты с кэшем работал. убери кэш, и все времена на порядок с лишним ухудшатся.

...можешь потыкаться в гугель с запросами ras/cas, хотя бы.

Какой нафиг кэш, если
а) я пишу 600 мегабайт в тесте
б) movntps - он, типа, мимо кэша

Лёша, ты три тезиса связать сумеешь ?
вот они:
*: Move packed single-precision floating-point values from xmm to m128 using non-temporal hint.
*: The non-temporal hint is implemented by using a write combining (WC) memory type protocol when writing the data to memory.
*: Write-combining is a limited caching optimization (more often used for RAM on devices such as graphics cards.)

так понятней ? ,-)

// ну и про прочие prefetch & write back тоже не забываем, ага

И че?

Думаешь 100 миллионов 16-байтных значений поместятся в кэше?

полагаю, что prefetch для того и был придуман, чтобы уменьшить кэш-промахи (а при опр. условиях и вовсе их избежать)... ты же отдаёшь себе отчёт в том, что prefetch выполняется асинхронно ? ...или таки - нет ?

Але, мы кажется запись обсуждали, причем тут prefetch?

нет, ты изначально рассматривал memory-to-memory copy. (напр. распаковку рава.)

кстати, WC нарушает "все мыслимые" полиси работы с памятью (не гарантирован порядок того и сего, не гарантирована "когерентность" и ты.ды.), так что...

Не, распаковку рава я тут (в этом письме) не обсуждал. Обсуждал read-modify-write (туда же), но там не movntps

а всё равно без кэша была бы полная труба...
ну, порядок можно было потерять запросто, я думаю.

На read-write - скорее да. По той причине, что prefetch для, как минимум, половины элементов достается на халяву.
С префетчем я еще поэкспериментирую, ибо интересно.

На чистый write - из поведения movntps видно, что большой кэш нафиг не нужен при длинных записях.

Если проводить аналогию с диском, то при линейных паттернах тоже не нужен кэш больше чем на дорожку.

Да, то что WC нарушает все подряд - это же отлично, нет вымывания кэша, нет оверхеда на синхронизацию кэшей в случае multicore. То есть если удается снабдить горшки независимыми данными (а с imaging - это несложно), то только за счастье.
fence только не забывать.

На видеокартах память так же устроена, я привычный.

а кого волнует "вымывание кэша" ? ты его чего, солить консервировать собрался ?

кэш оптимизирует работу с памятью. через уэш всё (с точки зрения доступа к мэмори) оптимальней происходит. а тем более при последовательной записи/чтении.

Интел пишет нам
"Writes to the WC memory type are not cached in the typical sense of the word cached"

Чем какбэ намекает нам, что у тебя sense какой-то не typical....

тогда уж цитируй дальше: "They are delayed in an internal buffer ... If software cares about data being delayed developers
must deliberately empty the WC buffers"
т.е. операция записи асинхронная.
это, знаешь ли, в свою очередь ну ни фига не typical sense of the memory writing ,-)

да, кстати, таки ты выполняется ли у тебя по коду "deliberately empty the WC buffers" ? ...а ты попробуй ,-)

Конечно асинхронная, если писать по 16 байт вместо 64, счастья не будет.

Заметим в скобках, что отключение L2/L3, которым ты тут пугаешь, на скорость этой записи вообще не повлияют.

Впрочем, если проинитить странички заранее, а не писать в только что выделенные, то скорость записи становится ближе к ожидаемой: 10-11Gb/sec в один поток и 16-17Gb/sec во все ядра.

Просто _mm_store_ps(куда, чего) в цикле.

А read, три умножения и два blend, write туда-же - 10-11Gb/sec.

ну вот видишь, сам же убедился продемонстрировал, что size cache does matter :-))

а теперь отключи L2/L3 кэш и насладись результатом (и приготовься к тому, что общая производительность системы упадёт на пару порядков... т.е. запасись попкорном терпением)

Зачем мне его отключать то? Я щупаю реальный перформанс на реальном таком полуторагигабайтном (уже) working set

а ты отключи, и вопросы отпадут сами собой :-))

кстати, коль уж кэш не при чём, то каким образом "если проинитить странички заранее, а не писать в только что выделенные, то скорость записи становится ближе к ожидаемой" ?
сам-то как думаешь ? :-))

Не-не, это другое.
Я могу взять у системы pinned memory (не факт что мне дадут полтора гига, ну значит гиг) - и тогда скорость сразу будет правильной.

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

То есть про кэш ты пишешь - это теоретически правильно, если бы у меня речь шла о мегабайтах т.е. о размерах сравнимых с размерами кэшей, то скорости были бы еще выше.

Собственно, хрен ли, с кэшом сейчас померяю тоже. В смысле, на маленьком working set, мегабайт 6

Меньше чем ожидал, но вот скорости кэшей
2-мегабайтный working set - ~20Gb/sec
80к - 28Gb/sec
все на одном ядре.
В несколько потоков параллелить бессмысленно, слишком они мелкие получаются.

Естественно, movaps а не non-temporal

это ж последовательный доступ.
в таком режиме кэш оч.эффективен. (WB/prefetch асинхронно и параллельно, т.е. как бы на халяву).

кстати, а нафига нужно связываться с этими non-temporal hint ? кто-нибудь профилировал это дело, или так, от балды решили, что "будет лучше" ?

Компилятор (интеловский) считает, что так лучше и ставит их вместо _mm_store_ps() если в цикле он один. А если два в соседние адреса (типа, поанроллил) - то не ставит.

ну, это он не понял, что по факту там будет последовательная запись...
наверное имеет смысл прагмами ему объяснить, чтобы не шибко умничал.

Ну вот Visual C++ в это место лепит movaps всегда.

А профайлер (VTune) показывает нам, что movntps на четверть быстрее. Что какбэ намекает нам, что интел то прав.

Кстати, 5.5 гигабайт в секунду на запись уже не пугают?

А то я и без всякого SSE научился почти столько получать, на чистых плюсах.

последовательное чтение/запись через кэш-то? - нет, совершенно не смущает/пугает.

...с памятью как с диском: "seek time" (т.е. ras/cas и сопутствующие латентности) дорогие, зато потом всё быстро. вот кэш и минимизирует эти "сиик таймззз", за счёт чего и приближается к производительности шины.

мммм...
оно, кажется, ни фига не "ПОСИКС", но тем не менее, насколько помню, можно запросить аллокацию с "коммитом", т.е. уже помэпленный кусок.
может, кстати, и даст прирост в производительности.

Я как-то не уверен, как ее можно так запросить, но потом можно залочить. Правда если попросил сначала больше присутствующей физической...

ну вот видишь, сам же убедился продемонстрировал, что size cache does matter :-))

Отсыпь травы, а то мне тебя не понять..

Он не увидит этого, это из ЖЖ приехало
(http://alextutubalin.livejournal.com/218527.html#comments)

ну да ладно..

Забивать-то зачем? Сделай reference C вариант, и дальше оптимизируй ассемблер, сколько влезет. На рантайме довольно дешево можно решить, что использовать.

PS: А потом, лет через N, останется убедиться, что С уже не медленнее ручного вариант и расслабиться, качаясь на кресле-качалке у камина :-D

Не, естественно reference C будет, как без него

Я скорее про то, что SSE2 варианта я специально делать не буду. Если вдруг появится сам для каких-то кусков, ну отлично тогда.

В автовекторизацию просто скалярного кода на C - я верю слабо.
И Интел, судя по всему, тоже не особо верит, впихивая повсюду свои библиотеки.

Ну например GCC кое-что умеет, но в принципе понятно, что руками можно сделать больше. Ты-бы, кстати, попробовал хохмы ради на libraw, получится-ли что-нибудь, и главное - сколько :)

А библиотеки - ессно ручной вылизанный код для определенных задач будет кошернее. Да и обычные программисты меньше косяков понаделают.

Я пробовал с интеловским компилятором. То, что он делает на хотспотах - мне не нравится, руками можно сильно лучше и с относительно небольшими затратами умственного труда.

Нравится/не нравится выходной ассемблер компилятора - не числительное. А вот померять времена выполнения и сравнить - сколько там процентов получится? Интересно ведь. Или это сложно/долго/муторно?

От разрешения генерировать SSE код (что 2, что 4.1) выигрыш какой-то совершенно копеечный, единицы процентов.
Вот от 64-бит - гораздо больше, т.к. куда регистры поюзать компилятор очень быстро находит.
И от смены компилятора с Visual Studio на 12-й интел - тоже заметный выигрыш.

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

Разрешения SSE мало, тот-же GCC надо отдельным специальным ключиком пинать, чтобы автовекторизовывать начал.

> И от смены компилятора с Visual Studio на 12-й интел - тоже заметный выигрыш.

А заметный - это сколько? И интел с автовекторизацией или как?

PS: Я почему столько вопросов задаю - уж очень любопытно выяснить, каких высот достиг прогресс. А у меня таких задач нет.
PPS: А OpenCL версию планируешь? :-)

Я развлекался именно с интелом, профайлер хороший имею на винде только.

Выигрыш VS2010/Intel я хорошо помню только для конкретного распаковщика хаффмана, который как раз не параллелится. Процентов 10, кажется. Автовекторизация хотспоты не лечит, не-хотспоты неинтересны.

Что касается OpenCL - да, планирую конечно, но для других мест и не сейчас (и наверное не LibRaw, а другой продукт). Проблема тут в том, что на карту льется гигабайт 5 в секунду и столько же обратно, поэтому нужно там делать сразу здоровый кусок работы, а не хотспоты.

скажем так, автовекторизация на icc работает, но для этого код должен быть написан в специальном виде (что по затратам фактически эквивалентно написанию кода на SSE intristics (фактически аналог ассемблера)).

Мы, кажется, о разном.

Автовекторизатор вроде как работает с существующим кодом и разве только просит прагмы расставить.
Я развлекался и особого смысла в результате не увидел, реальные хотспоты все одно придется переписывать, так почему не сразу на псевдоассемблер. Он хоть совместим со всем разумным.

Алексей, я извиняюсь что не по теме, но может подскажете что к чему. Сейчас ось XP SP3, хочу пересесть на семерку, но при этом хочется сохранить текующую ХП в виде виртуальной Оси из под семерки. То есть каким-то образом запаковать XP с софтом, а потом на чистой семерке поставить виртуальную машину, будь то родная от MS или вмваре... и в ней восстановаить прежнюю ХР. Такое возможно? Вообще многим как мне кажется это было бы удобно при переезде на новую Ось - и переезд быстрый, и ничего не потеряно и все работает. Может посоветуете какую лучше виртуалку ставить под семерку, даже если не удасться развернуть старую ХР, то придется новую ставить. Вмваре универсальна и позволяет разворачивать и операционки от MS и от Apple как я понимаю? Заранее спасибо)

Никогда такую задачу не решал.

Acronis backup умеет конвертировать backup в виртуальную машину, но я в это место никогда не заглядывал.

Вот кстати, может пригодится -
http://support.microsoft.com/kb/2280741

Само исправление не пробовал, зато уже видел в деле сам баг:
http://bugreports.qt.nokia.com/browse/QTBUG-11445
Студия влепила movaps на данные выровненные по восьми байтам, а не 16..

Задача: Таблично задана функция, необходимо вычислить вторую производную методом конечных разностей.
Выбранный подход к решению: Считываем в регистры 3 массива по 4 элемента, один со сдвигом влево (копируем с i-1 элемента: строка 65), второй с нулевым сдвигом (строка 66) и третий со сдвигом вправо (копируем с i+1 элемента: строка 67). Делаем вычисления. Копируем обратно. Программа работает, но sse версия работает много медленнее чем обычное решение.
Подозреваю, что проблема в использовании _mm_set_ps для копирования в регистры (строки 65-67) и организации обратного копирования из регистров (строки 69-71).

Вопрос: Как можно оптимизировать/сделать вменяемыми операции копирования?

Код:

// 1st_SSE.cpp
//

#include
#include
#include
#include
#include
#include
#include
using std::cout;
using std::cin;
using std::endl;

int main()
{
const int N=16*1024+1;
float *d2Ydx2=(float*)malloc(4*sizeof(float));
__m128 Ysse,YRsse,YMsse,YLsse;
__m128 rrs=_mm_set1_ps(-2.0f);
__m128 dX2s=_mm_set1_ps((N*N)/(6.28319f*6.28319f));
__m128 *d2Ydx2SSE=(__m128*) d2Ydx2;
float *Y1=(float*)malloc(N*sizeof(float));
float *Y=(float*)malloc(N*sizeof(float));
float *Y3=(float*)malloc(N*sizeof(float));
__m128 *Y2=(__m128*) Y;
timespec ts_beg, ts_end;

float rr,dX2;
const int SSEN=(N-1)/4;

dX2=(N*N)/(6.28319f*6.28319f);
rr=-2.0f;
//задаем исходную функцию
for (int i=0;i<=N;i++)
{
Y[i]=sin(i*(6.28319f/N));
Y3[i]=0.f;
Y1[i]=0.f;
}

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_beg);
// решение в лоб
for (int i=1;i<=N-1;i++)
{
Y1[i]=(Y[i-1]+rr*Y[i]+Y[i+1])*dX2;

};

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_end);
float time=(ts_end.tv_sec - ts_beg.tv_sec) + (ts_end.tv_nsec - ts_beg.tv_nsec)/1e9;
cout << "Time required = " << time <

А процессор какой? Почему SSE2?

Я бы делал так
1) Выравнивание (если у вас pre-i7 процессор, то это критично)
2) Очевидно что Y[i..i+3] можно прочитать один раз 128-битным чтением, а YLsse/YMsse/YRsse - заполнить из этого регистра через shuffle
3) Зачем store_ps в __m128 переменную, а потом поэлементная запись ее в Y3? Казалось бы, можно прямо в Y3[i] писать?
4) Это самое Y3[i+j] - пишется по 4 элемента на каждом цикле, после чего на следующем - затираются три из них? Я был неправ, у вас же во внешнем цикле i+=4;

P.S. Я позволил себе расставить в вашем комментарии теги code.

Еще очевидное улучшение вот тут:
- вам для работы нужно 6 элементов, с [i-1] по [i+4]
- но читать их все 6 внутри цикла не надо, вы можете [0] и [1] считать до цикла, потом считывать с [i+1] по [i+4] одним чтением, а [i-1] и [i] брать с первой итерации
- это потребует игр с выравниванием, на 16 байт должно быть выровнено [i+1]