Комментарии 50
но ведь есть arr.Cast<long>().Sum()
.
int intNumber = 10;
object o = intNumber;
long longNumber = (long) o;
В последней строчке будет ошибка.
Это где это "внутри перед кастом" они приводятся к object?
var arr = new []{ 5, 8, 9 };
var sum = arr.Sum();
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; и будет падение.
Попытка спустится на низкий уровень в той же яве, это просто боль.
Кроме этого ни что не мешает вынести критичный код в библиотеку (с + 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
На мой вгляд, у .NET есть большое преимущество перед C++, т.к. JIT компиляция происходит уже на клиентской машине, то компилятор может оптимизировать код под конкретный клиентский процессор, предоставляя максимальную производительность.И этот бред кочует из статьи в статью, в.т.ч по Яве, не имея никаких доказательств реального, а не возможного теоретически.
дНет будет иметь «большое преимущество перед C++», когда его хоть как то догонит =)
См и тут выше пример с memcmp, который даже с маршаллингом и без инлайна выигрывает вдвое-втрое.
В качестве пруфа предоставлю возможность транслировать код из статьи на С++ и проверить самостоятельно.
Может memcmp тоже SIMD внутри использует. Что-то уж больно быстр он.
Там только проверка на передаваемый размер, в общем случае сравнение идёт по 8 байт, объединённых в 4 группы.
Вот ассемблерный код, который генерит JIT для Intrinsics: pastebin.com/u77KXa5r
Он страшный, но я хз как его улучшить.
Тесты все ходят, в них я уверен, так что memcmp просто написан лучше, работает напрямую с памятью и мой код скорее всего где-то промахивается в кэше.
Очень странно.
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
Мне тоже этот момент вчера не понравился и я переписал сравнение без локальных переменных:
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
вроде смотрится лучше, но больше прироста скорости бенчмарк не показал
Потому что судя по операции сравнения
vpcmpeqb ymm0,ymm0,ymmword ptr [rax]
мы опять используем единственный регистр, а ArrayA выгружается в память обратно.
Видимо, компилятор не может (пока?) оптимизировать операцию с регистрами.
Забавно, что выгрузка в память в исходном ассемблерном коде идет через операцию vmovupd, предназначенную для работы с double.
Да, загрузка 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 ещё.
Отстается неясным, почему это не помогло по скорости.
В коде 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), чтобы судить однозначно.
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
Если сопоставить с трудозатратами и временем на разработку такого и кода и использованием высокроуневого АПИ на С# — последний очевидно будет более предпочтительным.
Я против заведомо ложных утверждений, одно из которых я и вынес в цитату.
Вообще, это один из приемов софистики.
В этой связи не проще ли выносить узкие части на С++? Компилятор (MS) достаточно умен, чтобы разрулить регистры, и можно достичь неплохой производительности, даже не прибегая к чтению руководств от Intel и анализа показателей latency для инструкций.
С++ может компилироваться на клиенте через llvm jit и как дотнет/ява оптимизироваться под конкретное железо но в целом к плюсам нужные ровные руки (чуть изменил и хоп — автовекторизация отвалилась) и много терпения для ожидания компиляции -_-
Единственная проблема с 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
Вы говорите о разных вещах, ни один компилятор ни одного языка (в том числе "быстрый" питон) не сможет автоматически векторизировать сложный код и ВСЕГДА придется скатываться до использования интринсиков прямо в коде
Умножение матриц и декомпозицию матриц (что это?) за вас написали на интрисиках. Эти самые интринсики и обозревал автор статьи — они для тех, кто хочет писать инструменты повверх которых такие как вы не будут задумываться о перфомансе. Нет универсального языка, который любую вашу формулу максимально оптимизирует сам
К примеру, вы можете взять базовые 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 это только вершина айсберга.
В этом году был Егор Богатов (см. dotnext-moscow.ru ) с докладом про перфоманс-оптимизации, но видео ещё не выложено в публичный доступ.
Видео Егора есть на сайте дотнекста в трансляции первого дня.
По теме: почему прямая реализация быстрее linq? Что они там наворотили, что стало медленнее реализации "в лоб"?
Небольшой обзор SIMD в .NET/C#