Pull to refresh

Comments 50

В c# можно без агрегатора посчитать сумму через linq

var arr = new []{ 5, 8, 9 };
var sum = arr.Sum();


А вообще c# это не производительность.
T IEnumerable.Sum() возвращает тот же тип, что и в коллекции. Aggregate позволяет этого избежать, чтобы не получить переполнения.
Хорошее замечание, правда в таком случае мы кастим весь массив, что еще сильнее бьет по производительности
Ваш код выбросит исключение, потому что внутри перед кастом элементы сначала приводятся к object. В данном случае можно заменить на arr.Select(i => (long)i).Sum();
Поддерживаю. Эта классика есть в Clr via C#. Частая проблема боксинга.
int intNumber = 10;
object o = intNumber;
long longNumber = (long) o;

В последней строчке будет ошибка.

Это где это "внутри перед кастом" они приводятся к object?


var arr = new []{ 5, 8, 9 };
var sum = arr.Sum();
Вот код Cast:
public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source)
{
    IEnumerable<TResult> typedSource = source as IEnumerable<TResult>;
    if (typedSource != null)
    {
        return typedSource;
    }
    
    if (source == null)
    {
        throw Error.ArgumentNull(nameof(source));
    }
    
    return CastIterator<TResult>(source);
}
private static IEnumerable<TResult> CastIterator<TResult>(IEnumerable source)
{
    foreach (object obj in source)
    {
        yield return (TResult)obj;
    }
}


Вот в этой строчке: yield return (TResult)obj; и будет падение.
C# это энтерпрайз. С кучей легаси кода и очень широкими возможностями.
Попытка спустится на низкий уровень в той же яве, это просто боль.

Кроме этого ни что не мешает вынести критичный код в библиотеку (с + asm) и вызывать уже оттуда.

Кроме этого ни что не мешает вынести критичный код в библиотеку (с + asm) и вызывать уже оттуда.


А вот люди, что полагаются на возможности SIMD из статьи ниже думают совсем иначе :)
Integrating native code libraries into .NET code for the sake of performance is not guaranteed to yield the benefits of native code optimization unless the custom code itself is also optimized. With the high-level programming models in .NET for SIMD, concurrency I/O, database access, network programming, and many other kinds of operations, developers could choose to develop all their HPC code in .NET and avoid incurring the cost and effort of integrating low-level native code into their applications.


Кроме этого ни что, кроме зависимости от конкретной ос и битности, а также наличия в команде нативного программиста под каждую ос, не мешает вынести критичный код в библиотеку (с + asm) и вызывать уже оттуда.

Fixed.
Теперь разберем по пунктам. По факту сейчас в продакшене существует примерно следующая система:
64bit
Windows, RH, Debian, Ubuntu
Intel Xeon
GPU Nvidia

Следующее утверждение. SIMD как правило используются в библиотеках, которые отлаживают годами, зачастую они и компилируются специальным компилятором.
Пытаться их переписать на C# это безумная утопия. А вот написать враппер или даже вызвать через exec. Вполне себе нормальное решение.

В итоге получаем:
1. Чистый красивый код на C#
2. «Вылизанный» низкоуровневый код на 2 платформы (amd64 Linux static, amd64 Windows static)

Кстати встречный вопрос а какой класс задач вы предлагаете решать на C# с помощью SIMD?

Только просьба без фанатизма, вспоминайте что ML, CV и прочее значительно легче решить на GPU
Кстати встречный вопрос а какой класс задач вы предлагаете решать на C# с помощью SIMD?

Сервер 3D игры, например. Только не надо говорить, что дотнет для этой задачи — «не торт». Сабжевая статья как раз рассказывает об очередном шаге к статусу «торт».
На мой вгляд, у .NET есть большое преимущество перед C++, т.к. JIT компиляция происходит уже на клиентской машине, то компилятор может оптимизировать код под конкретный клиентский процессор, предоставляя максимальную производительность.
И этот бред кочует из статьи в статью, в.т.ч по Яве, не имея никаких доказательств реального, а не возможного теоретически.

дНет будет иметь «большое преимущество перед C++», когда его хоть как то догонит =)
См и тут выше пример с memcmp, который даже с маршаллингом и без инлайна выигрывает вдвое-втрое.

В качестве пруфа предоставлю возможность транслировать код из статьи на С++ и проверить самостоятельно.
Я считаю, что основной фактор в большой разнице в скорости memcmp и моего кода на C# в том, что memcmp писали умные люди с кучей опыта, а код на C# писал я.

Может memcmp тоже SIMD внутри использует. Что-то уж больно быстр он.

Вот декмпилированный код memcmp, который используется в моих тестах: pastebin.com/bbPqQD1N
Там только проверка на передаваемый размер, в общем случае сравнение идёт по 8 байт, объединённых в 4 группы.
Вот ассемблерный код, который генерит JIT для Intrinsics: pastebin.com/u77KXa5r
Он страшный, но я хз как его улучшить.
Тесты все ходят, в них я уверен, так что memcmp просто написан лучше, работает напрямую с памятью и мой код скорее всего где-то промахивается в кэше.
Посмотрел код pastebin.com/u77KXa5r
Очень странно.
vmovdqu ymm0,ymmword ptr [rax] — загружаем 256 байт массива ArrayA в регистр ymm0
vmovupd ymmword ptr [rbp-0B0h],ymm0 — выгружаем его обратно в память

vmovdqu ymm0,ymmword ptr [rax] — загружаем 256 байт массива ArrayB в регистр ymm0
vmovupd ymmword ptr [rbp-70h],ymm0 — выгружаем его обратно в память
vmovupd ymm0,ymmword ptr [rbp-0B0h] — загружаем ранее выгруженный ArrayA в регистр ymm0
vpcmpeqb ymm0,ymm0,ymmword ptr [rbp-70h] — сравниваем ArrayA (ymm0) и ArrayB (который в памяти), результат заносим в ymm0

Это не Debug, часом? Не вижу других причин для использования единственного регистра ymm0
Это не Debug точно, т.к. этот asm код я получал через BenchmarkDotNet, а он отказывается работать в Debug. Данные гоняются туда-сюда из регистров в стэк и обратно как мне кажется из-за локальных переменных.
Мне тоже этот момент вчера не понравился и я переписал сравнение без локальных переменных:
byte* ptrA1 = ptrA + i;
byte* ptrB1 = ptrB + i;
if (Avx2.MoveMask(Avx2.CompareEqual(Avx2.LoadVector256(ptrA1), Avx2.LoadVector256(ptrB1))) != equalsMask) {
    return false;
}

И получилось уже такое:
00007ff9`17612cfa 4863c0 movsxd rax,eax
00007ff9`17612cfd 480345d0 add rax,qword ptr [rbp-30h]
00007ff9`17612d01 c4e17e6f00 vmovdqu ymm0,ymmword ptr [rax]
00007ff9`17612d06 488b45b0 mov rax,qword ptr [rbp-50h]
00007ff9`17612d0a c4e17d7400 vpcmpeqb ymm0,ymm0,ymmword ptr [rax]
00007ff9`17612d0f c4e17dd7c0 vpmovmskb eax,ymm0
00007ff9`17612d14 83f8ff cmp eax,0FFFFFFFFh
вроде смотрится лучше, но больше прироста скорости бенчмарк не показал
Да, загрузка ArrayB вроде уже одной инструкцией. А загрузку ArrayA не покажете?
Потому что судя по операции сравнения
vpcmpeqb ymm0,ymm0,ymmword ptr [rax]
мы опять используем единственный регистр, а ArrayA выгружается в память обратно.
Видимо, компилятор не может (пока?) оптимизировать операцию с регистрами.
Забавно, что выгрузка в память в исходном ассемблерном коде идет через операцию vmovupd, предназначенную для работы с double.
Вот полный asm: pastebin.com/MCDQ7mfr

Да, загрузка ArrayB вроде уже одной инструкцией. А загрузку ArrayA не покажете?… ArrayA выгружается в память обратно.


вот я тут вас не понял
00007ff9`17612cfa 4863c0 movsxd rax,eax
00007ff9`17612cfd 480345d0 add rax,qword ptr [rbp-30h]
00007ff9`17612d01 c4e17e6f00 vmovdqu ymm0,ymmword ptr [rax]
00007ff9`17612d06 488b45b0 mov rax,qword ptr [rbp-50h]
00007ff9`17612d0a c4e17d7400 vpcmpeqb ymm0,ymm0,ymmword ptr [rax]
00007ff9`17612d0f c4e17dd7c0 vpmovmskb eax,ymm0
00007ff9`17612d14 83f8ff cmp eax,0FFFFFFFFh

Разве не так получается:
  • в 17612d01 грузим в ymm0 256 бит из [rax] — часть из arrayA
  • в 17612d0a используем как второй аргумент часть arrayB из памяти. Не грузим её в отдельный регистр, но это сильно бьёт по скорости? По-моему так лучше.
  • в 17612d0f грузим маску в eax, потом сравниваем


Я надеюсь что компилятор «пока» не может оптимизировать это, т.к. .netcore 3 в preview ещё.
Да, здесь я неправ.
Отстается неясным, почему это не помогло по скорости.
После среды у меня будет время свободное вечером и я полностью разберу сравню код memcmp и, которые генерирует JIT, и попытаюсь понять в чём тормоза. Если кто-нибудь не сделает это раньше.
Посмотрел код memcmp. Основные затраты там в ветке if ( Size >> 5 ). Unrolled цикл, в котором обрабатывается 32 байта четыремя сравнениями int64.
В коде JIT, предположу, мешает обвязка.
С опозданием, но всё-таки опишу результаты попыток ускорить:
pastebin.com/JB4PvusV — код метода сравнения
pastebin.com/rJHH0GQn — лог BenchmarkDotNet
pastebin.com/LwDrtnmD — asm код.
Использовал NetCore 3.0 Preview 2.0
Тело цикла уменьшилось на 4 инструкции: выпилились вызовы функций + я по вашему совету закэшировал ArrayA.Length — vectorSize.
На неопределённое время точно прекращаю дальнейшие попытки, т.к. свободного времени почти нет.
Наконец-то посмотрел, спасибо.
Вроде с точки зрения векторизации все правильно.
Но последовательность инструкций
mov dword ptr [rbp-14h],eax
mov eax,dword ptr [rbp-14h]
удивляет все равно (в главном цикле переменная i загружается в регистр аж 4 раза). Такое впечатление, что IL транслируется в машинный код один к одному, без оптимизации.
Возможно, проще сделать цикл отдельно на long вместо разворачивания. По производительности там особо разницы не будет (сравниваются не более 24 байт), а код в asm сократится и ветвлений будет меньше.

С точки зрения измерений, думаю, 10000 дает слишком малое суммарное время — порядка сотен наносекунд, чтобы сравнивать Intrinsic и MemCmp. На 100000 MemCmp неожиданно обгоняет, а на 1000000 получаем слишком большой разброс результатов (StdDev сопоставимо с Mean), чтобы судить однозначно.
Я пробовал ещё ксорить векторы и потом через _mm256_testz_si256 сравнивать с вектором, заполенным единицами. Существенного прироста в скорости не было.
Возможно, имеет смысл кэшировать ArrayA.Length — vectorSize в локальную переменную. Там в цикле есть обращение
call 00007ff9`175eb570 get_ArrayA
Подозреваю, для вычисления длины массива на каждой итерации.

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

дНет будет иметь «большое преимущество перед C++», когда его хоть как то догонит =)


.net векторизация с оптимизациями недавними рантайма уже вполне сопоставим работает с unmanaged кодом, просто в статье этой нет сравнения с C/C++ — вот почитайте как эти же векторы для мандельброта в managed коде сравнивают с различными комбинациях c unmanaged код на C++. В целом он будет работать быстрее чем невекторизированный код на C/C++. Векторизированный нативный код хоть и быстрее, на с# для SIMD задач скейлится отлично и в режиме многопоточности работает быстрее чем однопоточный векторизированный нативный код.

www.codeproject.com/Articles/1223361/%2FArticles%2F1223361%2FBenchmarking-NET-Core-SIMD-performance-vs-Intel-IS

Если сопоставить с трудозатратами и временем на разработку такого и кода и использованием высокроуневого АПИ на С# — последний очевидно будет более предпочтительным.
Я не против того, что дНет дает сопоставимые результаты по сравнению с нативным кодом, что то типа О(1).

Я против заведомо ложных утверждений, одно из которых я и вынес в цитату.
Вообще, это один из приемов софистики.
Соглашусь, что векторизованный .net будет лучше невекторизованного С++ на больших объемах данных. Однако, вот смотрите, чем мы занимаемся в ветке комментариев выше — смотрим результат компиляции в asm. Т.е. чтобы писать эффективный код, все равно придется спускаться по абстракциям. И то, что нам приходится прибегать к unsafe — не так уж сильно отличается от С++ по безопасности и усилиям на отладку.
В этой связи не проще ли выносить узкие части на С++? Компилятор (MS) достаточно умен, чтобы разрулить регистры, и можно достичь неплохой производительности, даже не прибегая к чтению руководств от Intel и анализа показателей latency для инструкций.

С++ может компилироваться на клиенте через llvm jit и как дотнет/ява оптимизироваться под конкретное железо но в целом к плюсам нужные ровные руки (чуть изменил и хоп — автовекторизация отвалилась) и много терпения для ожидания компиляции -_-

В случае .net jit это только малая часть от данных результатов. Понятное дело, что если векторные оптимизации сами по себе бы не дали таких результатов в managed языке. В целом все развивается вокруг идеи low alloc типов и апи, что не аллоцируют. И высокоуровневое апи для работы с ними, что не требуют писать unmanaged код. Поэтому кроме нормальной поддержки SIMD у них еще большинство апи работают через memory pooling с буферами, что не создают GC pressure со стеком(работать с которым теперь, к слову, так же можно не прибегая к unmanaged коду) или native memory — опять же это все скрыто внутри удобных высокоуровневых Апи не требующих писать тон boilerplate кода как в С/C++ — скорее всего они будут иметь какой-то perf penalty но в целом будут работать соизмеримо и выглядеть куда предпочтительней.

Спасибо за разъяснения :D

Ох уж эти запятые! Сначала показалось что LINQ во много раз быстрее наивной реализации. На мой взгляд лучше использовать пробелы в качестве разделителей, либо вообще их не использовать.
Согласен. С пробелами стало более выразительно.

Единственная проблема с System.Runtime.Intrinsics — для максимальной эффективности вам придется сильно усложнить код, к примеру прежде чем бегать векторами по циклу надо выравнить данные, вот вам пример "простой" функции на C# для сложения массива чисел https://github.com/dotnet/machinelearning/blob/287bc3e9b84f640e9b75e7b288c2663a536a9863/src/Microsoft.ML.CpuMath/AvxIntrinsics.cs#L988-L1095 ;-)

.NET есть большое преимущество перед C++, т.к. JIT компиляция происходит… При этом программист для написания быстрого кода может оставаться в рамках одного языка и технологий
Чем python не устраивает и SIMD и CUDA и всё в рамках «одного языка и технологий» и не привязано гвоздями к .NET.
Как правило перед программистом стоит задача написать быстро, а не «быстрого кода». И для ускорения используются уже готовые оптимизированные библиотеки.

Тут основная проблема не в наличии векторных инструкций, а в отсутствии возможности упрощения кода, разделения алгоритма, организации представления данных, графа исполнения и оптимизаций. Что приводит к резкому усложнению кода. Попытки справиться с этим можно посмотреть в языке halide-lang.org

Вы говорите о разных вещах, ни один компилятор ни одного языка (в том числе "быстрый" питон) не сможет автоматически векторизировать сложный код и ВСЕГДА придется скатываться до использования интринсиков прямо в коде

Вы не поняли. Для написания быстрого кода программисту не надо лезть в дебри разных архитектур. Он просто использует декомпозицию матриц, умножает вектора, решает диф.уравнения, вызывает нейронные сети на фермах видиокарт. Ему нафиг не упало лезть и программировать плисы или копаться в AVX512. Если у вас супер компьютер с 2,397,824 ядрами вы не будете колупаться на C# с такой ерундой.

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

К примеру, вы можете взять базовые System.Numerics.Matrix4x4 в дотнете и перемножить их просто mat3 = mat1 * mat2; — дотнет сам за вас их векторизует ;-)

Проблема именно в том что архитектуры очень разные, и оптимизировать под каждую конкретно очень дорого и сложно. Поэтому оптимизированные библиотеки предоставляет производитель железа который эти инструкции туда добавил. И как правило он их туда добавляет не просто так, а для улучшения производительности именно этих библиотек.
software.intel.com/ru-ru/performance-libraries
developer.nvidia.com/gpu-accelerated-libraries
developer.amd.com/amd-cpu-libraries
projectne10.github.io/Ne10
www.ti.com/processors/digital-signal-processors/libraries/libraries.html
www.intel.com/content/www/us/en/software/programmable/sdk-for-opencl/overview.html
github.com/aws/aws-fpga
www.imgtec.com/developers/neural-network-sdk

И что бы не писать горы кода на C/C++ перед получением результата их оборачивают в библиотеки для языков типа python, что бы удобно было пользоваться тем кому нужно «ехать, а не шашечки».
И потом написание оптимизированных алгоритмов вручную это очень сложный и дорогостоящий процесс, требующий анализа огромного количества ограничений.
Попробуй-те оптимизировать реальную задачу, а не сложение векторов и вы поймёте что наличия доступа к SIMD это только вершина айсберга.
Код с Veсtors, помимо того, что более читаем и понятен, как мне кажется, лучше соответствует духу .NET. CLR мог бы как раз в зависимости от возможностей целевой машины транслировать его в SSE2/SSE4/AVX/AVX2/AVX-512.
Про SIMD хорошо в своё время рассказал на DotNext-е Sasha Goldshtein: www.youtube.com/watch?v=WeJ8b3WRSmM
В этом году был Егор Богатов (см. dotnext-moscow.ru ) с докладом про перфоманс-оптимизации, но видео ещё не выложено в публичный доступ.

Видео Егора есть на сайте дотнекста в трансляции первого дня.
По теме: почему прямая реализация быстрее linq? Что они там наворотили, что стало медленнее реализации "в лоб"?

Спасибо, познавательно.

Кстати, он озвучил одну из проблем intrinsics avx в .net — jit вставляет sse-инструкции vzeroupper, а смешение AVX и SSE кода, насколько я помню, может ухудшить производительность.

Незначительно, но лучше перебдеть, чем оставить мусор в регистрах ;-)

Плата за универсальность

Sign up to leave a comment.

Articles

Change theme settings