Комментарии 54
для чистоты эксперимента в первом тесте, я бы посоветовал вам в коде для "c" переместить выделение\освобождение памяти в цикл for (iter = 0 ; iter < iter_count; ++iter)
, наподобии кода в c#.
В случае C это будет последовательный блок неуправляяемой помяти, полученный с помощью malloc, а в случае C# мы рассмотрим как блок памяти находящийся в управляемой куче...
Т.е., фактически, сравниваете скорость выделения памяти операнционной системы со скоростью встроенного диспетчера памяти среды выполнения C#? :)
Отличный тест, "точный, как швейцарские часы":
for(var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
.
.
.
tmpArray = new bool[arraySize];
.
}
В замерах выделение памяти не участвует, gc на время выполнения не повлияет, так как он соберет большой объект только при выделении памяти, когда память выделенная для кучи закончится. Соберет он его вместе с поколением 2. Именно для того, чтобы сборка мусора не произошла в неудобный момент, выделений памяти вынесено за пределы измеряемого блока кода. Пробовал с ручной сборкой LOH, результаты не отличались. Оставил так для простоты кода.
Два простейших кода на Java и C показывают, что не всё так однозначно:


А почему сверху C++, а не C?
конечно заполнение массива интересное, сверху (int)ptr, что вообще непонятно какое число даёт (unspecified + overflow)
внизу в джаве тоже с оверфлоу заполнение
Если пойти посмотреть на генерируемый код, то (ожидаемо) подобные конструкции полностью исчезают, т.к. ничего не делают
https://godbolt.org/z/M6W43GG4E
Чтобы умный gcc не выбрасывал код достаточно сделать указатель статическим и присваивать не 1 а что то изменяющееся, например "i & 0xFF"
это называется shadowing. Ошибки нет
https://godbolt.org/z/jPf33Wz3r
всё по вашим советам, ничего не изменилось

И можно посмотреть. Как видим, в строке 9 происходит усечение значения из 64 в 8 бит без каких либо проблем, точно так же в моём коде происходит усечение значения адреса.
Код, в данном случае, не выбрасывается оптимизатором потому что указатель m передаётся в ф-ю free, и может быть использован в другой единице компиляции. По крайней мере он должен так делать. Если не делает, то оптимизатор ещё надо дорабатывать.
так вы сделали нечто очень странное, написали функцию, которую вызывать дважды == UB.
У вас просто очень странный код(которого не будет в реальности никогда), поэтому компилятор не оптимизировал
Компилятор знает что делает функция free, это по стандарту определено, поэтому компилятор может оптимизировать
У вас просто очень странный код(которого не будет в реальности никогда), поэтому компилятор не оптимизировал
Это было сделано только для целей отключения оптимизации затенения(shadowing).
Таких нереентрантных ф-й в стандартной библиотеке в достатке, к примеру: char *strtok(char *string, const char *delim);
Первый вызов - первый аргумент строка, второй вызов -- первый аргумент может быть NULL. И где по вашему сохраняется указатель на строку? Это первое.
Второе -- MSVC при таком коде выдаёт ошибку компиляции, и это правильно, т.к. статическая переменная внутри ф-ии инициализируется один раз. Очень странно, что godbolt это компилирует без ошибок.
N.B. Для тех кто не писал на ассемблере, весь С "== UB".
На будущее, однозначно рекомендую использовать вместо среднего медиану. Она как раз не чувствительна к выбросам, по типу тех, что бывали у вас в начале.
Компилятор C активно использует SIMD инструкции. А в вашем коде c# таких инструкций нет. Перепишите код с использованием Vector128/Vector256 и повторите тест. А то как-то нечестно получилось.
unsafe
{
const int typicalItarationsCount = 10;
const int arraySize = 1073741824;
var lineLength = sizeof(Vector256<byte>);
var linesCount = arraySize / lineLength;
var tmpArray = new byte[arraySize];
for (var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
var watch = new Stopwatch();
watch.Start();
fixed (byte* tmpArrayPtr = tmpArray)
for (long i = 0; i < linesCount; ++i)
{
var vector = Vector256.Create((byte) 1);
vector.Store(tmpArrayPtr + i * lineLength);
}
watch.Stop();
tmpArray = new byte[arraySize];
Console.WriteLine($"iter={iteration} seq time={watch.ElapsedMilliseconds}");
}
}
Примерно так это выглядит на моём пк. Цифры не идеальны т.к. в фоне работает много процессов. Я не ставил целью сделать идеальный тест. Лишь хотел показать тенденцию.


Если сравнивать C и c#, то только так. У меня нет настроенного рабочего окружения под C чтобы проверить разницу с шарпом. Оставляю это для вас.
В новых .NET кстати появились AVX512. Но они, к сожалению, доступны далеко не на всех современных железках, и не всегда работают хорошо.
unsafe
{
const int typicalItarationsCount = 10;
const int arraySize = 1073741824;
var linesCount = arraySize / sizeof(Vector512<byte>);
if (arraySize % sizeof(Vector512<byte>) != 0)
Console.WriteLine("Хвостик не обработаем :|");
var tmpArrayPtr = Marshal.AllocHGlobal(arraySize);
try
{
for (var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
var watch = new Stopwatch();
watch.Start();
var vector = Vector512.Create((byte)1);
Vector512<byte>* ptr = (Vector512<byte>*)tmpArrayPtr;
Vector512<byte>* end = ptr + linesCount;
Vector512<byte>* end4 = ptr + linesCount / 4 * 4;
while (ptr < end4)
{
*ptr++ = vector;
*ptr++ = vector;
*ptr++ = vector;
*ptr++ = vector;
}
while (ptr < end)
*ptr++ = vector;
watch.Stop();
Console.WriteLine($"iter={iteration} seq time={watch.ElapsedMilliseconds}");
}
}
finally
{
Marshal.FreeHGlobal(tmpArrayPtr);
}
}
iter=0 seq time=119
iter=1 seq time=40
iter=2 seq time=41
iter=3 seq time=41
iter=4 seq time=40
iter=5 seq time=41
iter=6 seq time=41
iter=7 seq time=41
iter=8 seq time=40
iter=9 seq time=40
Имхо — при подобных подходах C, C++ и C# должны уже показывать +‑ одинаковые результаты. Просто потому что это подразумевает отказ почти ото всех абстракций, которые дают языки, кроме каких‑то базовых. Дальше уже только ассемблер. Который в разном виде можно так или иначе запустить во всех языках.
Не стал упоминать Vector512 иначе бы пришлось этот маленький пример превратить в простыню из:
if (Vector512.IsHardwareAccelerated) {}
else if (Vector256.IsHardwareAccelerated) {}
else if (Vector128.IsHardwareAccelerated) {}
else if (Vector64.IsHardwareAccelerated) {}
else {}
Но это всё не имеет значения в контексте этого теста. Да и моё железо не поддерживает Vector512.
Вроде бы, нет тут SIMD
Код асемблера:
.file "test.c"
.text
.section .rodata.str1.8,"aMS",@progbits,1
.align 8
.LC0:
.string "memsize=%zxh sizeof(size_t)=%zx cache_line=%lu\n"
.section .rodata.str1.1,"aMS",@progbits,1
.LC1:
.string "unable to allocate memory\n"
.LC2:
.string "iter=%d seq time=%lu\n"
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB51:
.cfi_startproc
endbr64
pushq %r15
.cfi_def_cfa_offset 16
.cfi_offset 15, -16
movl $190, %edi
pushq %r14
.cfi_def_cfa_offset 24
.cfi_offset 14, -24
pushq %r13
.cfi_def_cfa_offset 32
.cfi_offset 13, -32
pushq %r12
.cfi_def_cfa_offset 40
.cfi_offset 12, -40
pushq %rbp
.cfi_def_cfa_offset 48
.cfi_offset 6, -48
pushq %rbx
.cfi_def_cfa_offset 56
.cfi_offset 3, -56
subq $24, %rsp
.cfi_def_cfa_offset 80
call sysconf@PLT
movl $8, %ecx
movl $1073741824, %edx
leaq .LC0(%rip), %rsi
movq %rax, %r8
movq %rax, %rbx
movl $2, %edi
xorl %eax, %eax
call __printf_chk@PLT
leaq 1073741824(%rbx), %rdi
call malloc@PLT
movq %rax, 8(%rsp)
testq %rax, %rax
je .L14
xorl %edx, %edx
movq %rbx, %r14
xorl %r12d, %r12d
movabsq $2361183241434822607, %r15
divq %rbx
movq 8(%rsp), %rax
subq %rdx, %r14
addq %rax, %r14
leaq 1073741824(%r14), %rbp
.p2align 4,,10
.p2align 3
.L6:
call clock@PLT
movq %rax, %r13
testq %rbx, %rbx
je .L4
movq %r14, %rcx
.p2align 4,,10
.p2align 3
.L5:
movq %rcx, %rdi
movq %rbx, %rdx
movl $1, %esi
call memset@PLT
movq %rax, %rcx
addq %rbx, %rcx
cmpq %rbp, %rcx
jb .L5
.L4:
call clock@PLT
movl $2, %edi
subq %r13, %rax
movq %rax, %rsi
imulq %r15
xorl %eax, %eax
sarq $63, %rsi
sarq $7, %rdx
subq %rsi, %rdx
leaq .LC2(%rip), %rsi
movq %rdx, %rcx
movl %r12d, %edx
addl $1, %r12d
call __printf_chk@PLT
cmpl $10, %r12d
jne .L6
movq 8(%rsp), %rdi
call free@PLT
xorl %eax, %eax
.L1:
addq $24, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 56
popq %rbx
.cfi_def_cfa_offset 48
popq %rbp
.cfi_def_cfa_offset 40
popq %r12
.cfi_def_cfa_offset 32
popq %r13
.cfi_def_cfa_offset 24
popq %r14
.cfi_def_cfa_offset 16
popq %r15
.cfi_def_cfa_offset 8
ret
.L14:
.cfi_restore_state
movq stderr(%rip), %rcx
movl $26, %edx
movl $1, %esi
leaq .LC1(%rip), %rdi
call fwrite@PLT
orl $-1, %eax
jmp .L1
.cfi_endproc
.LFE51:
.size main, .-main
.ident "GCC: (Ubuntu 13.2.0-23ubuntu4) 13.2.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
Благодарю за комментарий. Ваш код проверил, цифры те же +- что и у обычного массива. Обязательно разберусь, можно ли с помощью SIMD ускориться.
Отметил на скрине avx инструкции.
Можно здесь посмотреть. https://sharplab.io/

Ваш код проверил, цифры те же +- что и у обычного массива.
Насчёт этого не понял. Этот код с Vector256 ну просто не может быть по скорости таким же как как ваш из статьи.
upd: не заметил, что вы написали в комментарии скомпилированный код C, а не c#. На C я не работал. Поэтому подумал, что там есть SIMD инструкции после компиляции и не проверил свою гипотезу.
.file "test.c"
- речь шла про сишный ассемблер.
Ответ для C - думаю потому что компилятор решил сгенерировать вызов memset
и все SIMD оптимизации идут уже в нём.
Gcc всё сводит к вызову memset (call memset@PLT), видимо у неё внутри SIMD есть.
Можно Clang смотреть, там SIMD в явном виде (строки 72-74): https://gcc.godbolt.org/z/PPPz6xv6s
т.е. тестировался вывод в консоль (prinf внутри цикла "бенчмарка") и в статье про заполнение памяти не упомянуты memset и memcpy, а всё происходило внутри функции main, которая для гцц особая и оптимизируется иначе.
Что ж, эталонный бенчмарк
Вы код читали? Вывод и заполнение памяти вне измеряемого цикла же.
ну раз вы читали, то может вы расскажете почему компилятору нельзя просто удалить этот код заполнения памяти, если она нигде не используется
Вы многого хотите от компилятора, чтоб он оценивал используемость памяти в куче)
Это уже автоматическое управление памятью будет)
Вы переоцениваете сложность задачи
https://godbolt.org/z/Pad8Mc3ca
Как видно, никаких манипуляций с памятью, в том числе её выделения, нет
И что, он у вас это заоптимизировал, хотите сказать?
Я чуть чуть модифировал ваш код и он уже ничего не заоптимизировал
void foo() {
int8_t* m = (int8_t*)malloc(1024*1024);
printf("hello");
for (int i = 0; i < 1000; ++i)
for (int i = 0; i < 1024*1024; ++i)
m[i] = 1 + m[i];//Добавил обращение внутри
printf("world");
free(m);
}
Думается мне, в вашем случае он справился не по тому, что измененная память нигде не используется, а потому что сама переменная m нигде не используется
Речь идет о C, а не о C++. Проверьте вышеприведенный код с этим языком
компилируется тем же компилятором, по практически тем же правилам, что там может измениться?
https://godbolt.org/z/G69hdzEdc
Вы тестируете оптимизатор С? Да он довольно хорош.
Код на С, довольно вырвиглазный.
Пожалуйста не используйте эти стопаотчи для тестирования производительности.
Для бенчмарков в .net есть уже либо - BenchmarkDotnet ей и пользуйтесь
Для "C" самым шустрым на данный момент является всё же не gcc-13, а clang-18.
Конкретно в случае моей машины получаем "avg seq time=309.10" на gcc против "avg seq time=134.50" на clang в случае последовательной записи в память
Вспомнил, что в c# есть Span<T>.Fill. Для C использовал компилятор clang-19.1.0 с аргументом "-O3". Результат, абсолютно одинаковая скорость. Так что выводы в статье абсолютно неверные, как и код для тестирования.


Вы правы, SIMD-оптимизации не были учтены, и это сильно портит сравнение. Однако Span<T>.Fill() я бы, все же, сравнил с memcpy. Хотелось посмотреть на то, как оптимизируются банальные вещи, без специфического тюнинга кода под задачу.
Статья вообще ни очём.
Вы тестируете один единственный очень-очень-очень редкий кейс, который в живой природе не встречается примерно никогда и делаете какие-то выводы на его основе.
С таким же успехом можно было засечь время выполнения пустого цикла (тут возможны сюрпризы разумеется) или умножение/сложение одного числа в цикле.
Попробуйте решить задачу 123456789^123456789. Сколько это займёт на c в GMP и c# в biginteger. Будет время подумать :)
Код конечно адский и тестирует компилятор а не язык . Вы бы еще 2+2 тестировали в цикле - различия были бы еще удивительнее
Может ли C# догнать C?