Pull to refresh
8
0
Send message

Если понимать, как работает векторизация, то можно "наивный" код довольно легко дотюнить до векторизуемого состояния. Получается короче интринсиков и совместимо с разными системами команд.
https://gcc.godbolt.org/z/jbrqGKxnx
(хвост проигнорирован для простоты)

Gcc всё сводит к вызову memset (call memset@PLT), видимо у неё внутри SIMD есть.
Можно Clang смотреть, там SIMD в явном виде (строки 72-74): https://gcc.godbolt.org/z/PPPz6xv6s

А это справедливо для каких именно чисел: single, double или extended?

Single и Double. Extended в SSE вообще нет, так что в x64 Extended = Double.

Для Single желательно использовать {$EXCESSPRECISION OFF}, иначе расчёты пытается делать в Double и тормозит из-за постоянных преобразований.

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

не вытягивает Паскаль сложные алгоритмы работы со звуком.

Если у вас расчёты в плавающей точке, то справедливости ради, в современных Дельфях в x64 стало получше, т.к. перешли с FPU на SSE2. Можно рассчитывать на ускорение раза в 2.
Но конечно, у С/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 секунды.

И что не так?
  function Add(Const v1, v2: TVector): TVector; overload; inline;
  begin
    Result.x := v1.x + v2.x;
    Result.y := v1.y + v2.y;
    Result.z := v1.z + v2.z;
  end;

  function Dot(Const v1, v2: TVector): TFloat; overload; inline;
  begin
    Result := v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
  end;

  function Dot(Const v1: TVector): TFloat; overload; inline;
  begin
    Result := v1.x * v1.x + v1.y * v1.y + v1.z * v1.z;
  end;

  function Init(x,y,z : TFloat): TVector; inline;
  begin
    Result.x := x; Result.y := y; Result.z := z;
  end;

// в tracer:
        p := o + TVector.Create(-k, 0, -(j + 4));
        b := p * d;
        c := p * p - 1;
        q := b * b - c;
// ->
        p := Add(o, Init(-k, 0, -(j + 4)));
        b := Dot(p, d);
        c := Dot(p) - 1;
        q := b * b - c;

Новые (другие) типы. Также, использовать новую математику, интринсики и т.д.

Какие конкретно? Про 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-код сложнее.

Советы компиляторы уже дают, в 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.

код
#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

Information

Rating
Does not participate
Registered
Activity