Как стать автором
Обновить

Комментарии 54

для чистоты эксперимента в первом тесте, я бы посоветовал вам в коде для "c" переместить выделение\освобождение памяти в цикл for (iter = 0 ; iter < iter_count; ++iter), наподобии кода в c#.

и чтобы для си компилятор не выкинул заполнение памяти, добавьте volatile

volatile const char c = 1;

Может, сразу sleep(1) в цикл добавить для надёжности? :-)

Лучше уж код на C# нормально написать, не создавая 100500 массивов в цикле.

ну только если вам это нужно для теста

В случае C это будет последовательный блок неуправляяемой помяти, полученный с помощью malloc, а в случае C# мы рассмотрим как блок памяти находящийся в управляемой куче...

Т.е., фактически, сравниваете скорость выделения памяти операнционной системы со скоростью встроенного диспетчера памяти среды выполнения C#? :)

а если учесть, что диспетчер тоже обращается к ОС, чтоб та выделила память, то еще интереснее получается: просто смотрим на скорость быстродействия диспетчера в вакууме.

Отличный тест, "точный, как швейцарские часы":

for(var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
    .
    .
    .
    tmpArray = new bool[arraySize];
    .
}

В замерах выделение памяти не участвует, gc на время выполнения не повлияет, так как он соберет большой объект только при выделении памяти, когда память выделенная для кучи закончится. Соберет он его вместе с поколением 2. Именно для того, чтобы сборка мусора не произошла в неудобный момент, выделений памяти вынесено за пределы измеряемого блока кода. Пробовал с ручной сборкой LOH, результаты не отличались. Оставил так для простоты кода.

А почему сверху C++, а не C?

Вы думаете что то изменится? Увы, нет:

Лайк за шрифт консоли

конечно заполнение массива интересное, сверху (int)ptr, что вообще непонятно какое число даёт (unspecified + overflow)
внизу в джаве тоже с оверфлоу заполнение

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

Чтобы умный gcc не выбрасывал код достаточно сделать указатель статическим и присваивать не 1 а что то изменяющееся, например "i & 0xFF"

изменилось
изменилось

И можно посмотреть. Как видим, в строке 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}");
    }
}

Примерно так это выглядит на моём пк. Цифры не идеальны т.к. в фоне работает много процессов. Я не ставил целью сделать идеальный тест. Лишь хотел показать тенденцию.

Мой код с Vector256
Мой код с Vector256
Код из статьи
Код из статьи

Если сравнивать 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 оптимизации идут уже в нём.

Похоже что так и есть. Chatgpt говорит, что: memset относится к языку C как часть стандартной библиотеки, но её оптимизация зависит от компилятора и библиотеки, связанной с ОС.

Без 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

Как видно, никаких манипуляций с памятью, в том числе её выделения, нет

И что, он у вас это заоптимизировал, хотите сказать?

нуу, да

Странно конечно

А если я потом по этому адресу напрямую что то достану?

потом это когда, после free?

В другом потоке, например

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

Я чуть чуть модифировал ваш код и он уже ничего не заоптимизировал

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 тестировали в цикле - различия были бы еще удивительнее

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации