Современное сообщество программистов разбито на два лагеря - на тех, кто любит языки программирования с управляемой памятью, и тех кто их не любит. Два лагеря яро спорят друг с другом, ломая копья по поводу преимуществ в каком-то из аспектов программирования. Языки с неуправляемой памятью представляются как более быстрые, управляемые, контролируемые. А языки с управляемой памятью считаются более удобными в разроботке, в то время как их отставание по скорости выполнения и потребляемой памяти считается несущественным. В этой статье мы проверим, так ли это на самом деле. Со стороны олдскульных языков программирования выступит мастодонт мира разработки - С.
Сторону языков последних поколений будет представлять С#.
Статья носит ознакомительный характер и не претендует на комплексное сравнение. Полноценного тестирования проведено не будет, но будут приведены тесты, которые сможет повторить любой разработчик на своем компьютере.
Детали
Оба языка будут участвовать в последних своих LTC версиях на момент написания статьи.
С = gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0
C# = C# 12, NET 8.0
Для сравнения будет использоваться машина с операционной системой Linux
Operating System: Ubuntu 24.04.1 LTS
Kernel: Linux 6.8.0-48-generic
Architecture: x86-64
CPU
*-cpu
description: CPU
product: AMD Ryzen 7 3800X 8-Core Processor
vendor: Advanced Micro Devices [AMD]
physical id: 15
bus info: cpu@0
version: 23.113.0
serial: Unknown
slot: AM4
size: 2200MHz
capacity: 4558MHz
width: 64 bits
clock: 100MHz
capabilities: lm fpu fpu_exception wp vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp x86-64 constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb cat_l3 cdp_l3 hw_pstate ssbd mba ibpb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 cqm rdt_a rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local clzero irperf xsaveerptr rdpru wbnoinvd arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif v_spec_ctrl umip rdpid overflow_recov succor smca sev sev_es cpufreq
configuration: cores=8 enabledcores=8 microcode=141561889 threads=16
Memory
Getting SMBIOS data from sysfs.
SMBIOS 3.3.0 present.Handle 0x000F, DMI type 16, 23 bytes
Physical Memory Array
Location: System Board Or Motherboard
Use: System Memory
Error Correction Type: None
Maximum Capacity: 128 GB
Error Information Handle: 0x000E
Number Of Devices: 4Handle 0x0017, DMI type 17, 84 bytes
Memory Device
Array Handle: 0x000F
Error Information Handle: 0x0016
Total Width: Unknown
Data Width: Unknown
Size: No Module Installed
Form Factor: Unknown
Set: None
Locator: DIMM 0
Bank Locator: P0 CHANNEL A
Type: Unknown
Type Detail: UnknownHandle 0x0019, DMI type 17, 84 bytes
Memory Device
Array Handle: 0x000F
Error Information Handle: 0x0018
Total Width: 64 bits
Data Width: 64 bits
Size: 16 GB
Form Factor: DIMM
Set: None
Locator: DIMM 1
Bank Locator: P0 CHANNEL A
Type: DDR4
Type Detail: Synchronous Unbuffered (Unregistered)
Speed: 3200 MT/s
Manufacturer: Unknown
Serial Number: 12030387
Asset Tag: Not Specified
Part Number: PSD416G320081
Rank: 1
Configured Memory Speed: 3200 MT/s
Minimum Voltage: 1.2 V
Maximum Voltage: 1.2 V
Configured Voltage: 1.2 V
Memory Technology: DRAM
Memory Operating Mode Capability: Volatile memory
Firmware Version: Unknown
Module Manufacturer ID: Bank 6, Hex 0x02
Module Product ID: Unknown
Memory Subsystem Controller Manufacturer ID: Unknown
Memory Subsystem Controller Product ID: Unknown
Non-Volatile Size: None
Volatile Size: 16 GB
Cache Size: None
Logical Size: NoneHandle 0x001C, DMI type 17, 84 bytes
Memory Device
Array Handle: 0x000F
Error Information Handle: 0x001B
Total Width: Unknown
Data Width: Unknown
Size: No Module Installed
Form Factor: Unknown
Set: None
Locator: DIMM 0
Bank Locator: P0 CHANNEL B
Type: Unknown
Type Detail: UnknownHandle 0x001E, DMI type 17, 84 bytes
Memory Device
Array Handle: 0x000F
Error Information Handle: 0x001D
Total Width: 64 bits
Data Width: 64 bits
Size: 16 GB
Form Factor: DIMM
Set: None
Locator: DIMM 1
Bank Locator: P0 CHANNEL B
Type: DDR4
Type Detail: Synchronous Unbuffered (Unregistered)
Speed: 3200 MT/s
Manufacturer: Unknown
Serial Number: 120304DD
Asset Tag: Not Specified
Part Number: PSD416G320081
Rank: 1
Configured Memory Speed: 3200 MT/s
Minimum Voltage: 1.2 V
Maximum Voltage: 1.2 V
Configured Voltage: 1.2 V
Memory Technology: DRAM
Memory Operating Mode Capability: Volatile memory
Firmware Version: Unknown
Module Manufacturer ID: Bank 6, Hex 0x02
Module Product ID: Unknown
Memory Subsystem Controller Manufacturer ID: Unknown
Memory Subsystem Controller Product ID: Unknown
Non-Volatile Size: None
Volatile Size: 16 GB
Cache Size: None
Logical Size: None
Поскольку главным отличием одного языка от другого является управляемая память, на обращение с этой памятью мы и будем смотреть. А именно - будем смотреть на скорость записи в оперативную память.
Тестов на которых можно проверить разницу множество, но в рамках наших тестов мы будем заполнять последовательный блок памяти размером 1 GB.
В случае C это будет последовательный блок неуправляяемой помяти, полученный с помощью malloc, а в случае C# мы рассмотрим как блок памяти находящийся в управляемой куче, так и блок неуправляемой памяти в адресном пространстве процесса.
C# позволяет нам работать с неуправляемой памятью.
За счет чего может появиться разница во времени исполнения этой операции?
Код, который мы будем сравнивать, в конечном итоге превратится в инструкции для процессора, которые этот процессор будет выполнять. Однако, когда мы говорим о С, мы понимаем, что компилятор может оптимизировать написанный нами код. В случае же C# ситуация еще сложнее. В обычных условиях код будет скомпилирован в промежуточный язык CIL, который затем будет с помощью компиляции реального времени (JIT) скомпилирован в набор инструкций, которые будут исполняться. Код может быть оптимизирован на обоих этапах.
Именно сравнение этих оптимизаций двух языков программирования нам и интересно.
Однако, кроме этих оптимизаций на время выполнения нашего кода может влиять большое число факторов, например, особенности реализации самого процессора.
Тест №1
Для начала посмотрим на ситуацию без оптимизаций
Будем смотреть на итеративную запись блоками по 1 байту. Код чуть сложнее, чем требуется для теста. Это сделано для того, чтобы результаты времени его работы можно было сравнивать с другими результами, полученными в рамках этой статьи.
Первым выполним код на C
Просто скомпилируем его, не указывая компилятору, что нужно применить оптимизации
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <stddef.h>
#define MEMSIZE (1l << 30)
#define CLOCK_IN_MS (CLOCKS_PER_SEC / 1000)
#define ITERATIONS 10
int main(int argc, char **argv)
{
const size_t mem_size = MEMSIZE;
const size_t cache_line_size = sysconf (_SC_LEVEL1_DCACHE_LINESIZE);
clock_t start_clock;
long diff_ms = 0;
char *mem, *arr, *stop_addr, *ix_line;
ptrdiff_t ix_char = 0;
const char c = 1;
int iter = 0;
const int iter_count = ITERATIONS;
printf("memsize=%zxh sizeof(size_t)=%zx cache_line=%lu\n",
mem_size, sizeof(mem_size), cache_line_size
);
if (!(mem = malloc(mem_size + cache_line_size))){
fprintf(stderr, "unable to allocate memory\n");
return -1;
}
arr = mem + cache_line_size - (long)mem % cache_line_size;
stop_addr = arr + mem_size;
for (iter = 0 ; iter < iter_count; ++iter) {
start_clock = clock();
for ( ix_line = arr; ix_line < stop_addr ; ix_line += cache_line_size) {
for (ix_char = 0 ; ix_char < cache_line_size ; ++ix_char) {
*(ix_line + ix_char) = c;
}
}
diff_ms = (clock() - start_clock) / CLOCK_IN_MS;
printf("iter=%d seq time=%lu\n", iter, diff_ms);
}
free(mem);
return 0;
}
Результаты:
Среднее время: 2700 ms
iter=0 seq time=2177
iter=1 seq time=2765
iter=2 seq time=2765
iter=3 seq time=2797
iter=4 seq time=2781
iter=5 seq time=2743
iter=6 seq time=2791
iter=7 seq time=2743
iter=8 seq time=2695
iter=9 seq time=2739
Среднее время больше указанного, так как большой вклад дает первая итерация с маленьким значением.
Теперь посмотрим на C# и массив в куче
using System.Diagnostics;
const int typicalItarationsCount = 10;
const int arraySize = 1073741824;
const int lineLength = 64;
const int linesCount = arraySize / lineLength;
var tmpArray = new bool[arraySize];
for(var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
var watch = new Stopwatch();
watch.Start();
for(long i = 0; i < linesCount; ++i)
{
for(long j = 0; j < lineLength; ++j)
{
tmpArray[i * lineLength + j] = true;
}
}
watch.Stop();
tmpArray = new bool[arraySize];
Console.WriteLine($"iter={iteration} seq time={watch.ElapsedMilliseconds}");
}
Результаты:
Среднее время: 446 ms
iter=0 seq time=764
iter=1 seq time=766
iter=2 seq time=362
iter=3 seq time=362
iter=4 seq time=369
iter=5 seq time=362
iter=6 seq time=364
iter=7 seq time=372
iter=8 seq time=368
iter=9 seq time=370
На самом деле среднее время меньше, так как большой вклад дают первые две итерации. Если выполнить большее число итераций, среднее время уменьшится.
А теперь посмотрим на неуправляемую память в C#
Для работы с указателями в C# необходимо пометить блок кода ключевым словом "unsafe", а так же добавить в файл .csproj блок указывающий, что сборка будет работать с таким кодом.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
using System.Diagnostics;
using System.Runtime.InteropServices;
unsafe
{
const int typicalItarationsCount = 10;
const int arraySize = 1073741824;
const int lineLength = 64;
const int linesCount = arraySize / lineLength;
for(var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
bool* buffer = (bool*)NativeMemory.Alloc((nuint) arraySize, sizeof(bool));
var readPtr = buffer;
var endPtr = buffer + arraySize;
var watch = new Stopwatch();
watch.Start();
for(long i = 0; i < linesCount; ++i)
{
for(long j = 0; j < lineLength; ++j)
{
*readPtr = true;
++readPtr;
}
}
watch.Stop();
NativeMemory.Free(buffer);
Console.WriteLine($"iter={iteration} seq time={watch.ElapsedMilliseconds}");
}
}
Результаты:
Среднее время: 691 ms
iter=0 seq time=696
iter=1 seq time=704
iter=2 seq time=694
iter=3 seq time=689
iter=4 seq time=686
iter=5 seq time=696
iter=6 seq time=684
iter=7 seq time=692
iter=8 seq time=685
iter=9 seq time=688
Без применения специальных оптимизаций, C проиграл соревнование по скорости в 7 раз по сравнению с массивами в куче C#, и в 4 раза по сравнению с использованием неуправляемой помяти в C#. Результаты уже интересны.
Тест №2
Теперь скомпилируем C код с максимальными возможными оптимизациями
- используем аргумент командной строки для gcc "-Wall -O4"
Результаты:
Среднее время: 118 ms
iter=0 seq time=448
iter=1 seq time=81
iter=2 seq time=82
iter=3 seq time=83
iter=4 seq time=82
iter=5 seq time=82
iter=6 seq time=82
iter=7 seq time=81
iter=8 seq time=81
iter=9 seq time=82
Среднее время меньше, так как первая итерация с большим временем выполнения оказывает большой эффект. Это происходит потому, что операционная система фактически выделяет память только при записи.
Как и предполагалось, оптимизированный код на C показывает впечатляющие результаты
Но эти результаты впечатляют по сравнению с результатами неоптимизированного специально кода на C#.
Попробуем использовать оптимизации в C# при работе с массивом в куче
Для этого необходимо добавить в .csproj файл секцию, включающую оптимизации выполняемые компилятором
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<Optimize>true</Optimize>
</PropertyGroup>
</Project>
Результаты:
Среднее время: 603 ms
iter=0 seq time=953
iter=1 seq time=948
iter=2 seq time=515
iter=3 seq time=522
iter=4 seq time=520
iter=5 seq time=517
iter=6 seq time=516
iter=7 seq time=520
iter=8 seq time=507
iter=9 seq time=510
Попробуем использовать оптимизации в C# при работе с неуправляемой помятью
Результаты:
Среднее время: 694 ms
iter=0 seq time=690
iter=1 seq time=687
iter=2 seq time=686
iter=3 seq time=694
iter=4 seq time=691
iter=5 seq time=702
iter=6 seq time=697
iter=7 seq time=704
iter=8 seq time=695
iter=9 seq time=695
Видно, что попытка указать компилятору C#, что код нужно оптимизировать, к улучшению результатов не приводит.
Может быть дело в JIT-компиляции? Последяя версия C# позволяет использовать AOT-компиляцию.
Тест №3
Попробуем скомпилировать C# код нативно для нашего компьютера.
Для исполнения такого файла нам не нужен будет dotnet
Для этого .csproj должен содержать секцию добавляющую нативную публикацию
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup>
<PublishAot>true</PublishAot>
<OptimizationPreference>Speed</OptimizationPreference>
</PropertyGroup>
</Project>
Результаты для массивов в куче:
Среднее время: 548 ms
iter=0 seq time=932
iter=1 seq time=905
iter=2 seq time=453
iter=3 seq time=450
iter=4 seq time=453
iter=5 seq time=464
iter=6 seq time=452
iter=7 seq time=459
iter=8 seq time=452
iter=9 seq time=456
Первые две итерации опять сильно влияют на результат.
Результаты для неуправляемой памяти:
Среднее время; 827 ms
iter=0 seq time=822
iter=1 seq time=822
iter=2 seq time=828
iter=3 seq time=829
iter=4 seq time=826
iter=5 seq time=828
iter=6 seq time=827
iter=7 seq time=829
iter=8 seq time=831
iter=9 seq time=826
Прироста производительности тоже не наблюдается
Вывод
C# проигрывает C при последовательной записи в оперативную память примерно в 8 раз. Это происходит из-за того, что оптимизации компилятора C превосходят оптимизации которые претерпевает C# код, превращаясь в машинные коды. Однако, эти оптимизации бесполезны при непоследовательной записи в память, что будет видно в следующем тесте. Сторонние факторы, такие как физическая реализация процессора, влияют на многие операции сильнее, чем разница в программах, написанных на этих языках
Немного теории
Центральным элементом современного компьютера является процессор. У процессора есть кеш-линии - последовательные кусочки памяти, в которые загружаются данные, с которыми процессор будет работать. Загрузка кеш-линии довольно дорогая операция, поэтому, если возможно, такие операции нужно минимизировать. Предполагаем, что для заполнения блока оперативной памяти, с последовательной записью данных, число загрузок данных в кеш-линии процессора и последующих копирований этих данных в оперативную память будет минимально. А при непоследовательной записи в память, когда для каждой следующей итерации кеш-линию необходимо перезагружать, - максимально.
Поэтому проведем следующий тест.
Тест №4
Посмотрим на C код не последовательно пишущий в память
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <stddef.h>
#define MEMSIZE (1l << 30)
#define CLOCK_IN_MS (CLOCKS_PER_SEC / 1000)
#define ITERATIONS 10
int main(int argc, char **argv)
{
const size_t mem_size = MEMSIZE;
const size_t cache_line_size = sysconf (_SC_LEVEL1_DCACHE_LINESIZE);
clock_t start_clock;
long diff_ms = 0;
char *mem, *arr, *stop_addr, *ix_line;
ptrdiff_t ix_char = 0;
const char c = 1;
int iter = 0;
const int iter_count = ITERATIONS;
printf("memsize=%zxh sizeof(size_t)=%zx cache_line=%lu\n",
mem_size, sizeof(mem_size), cache_line_size
);
if (!(mem = malloc(mem_size + cache_line_size))){
fprintf(stderr, "unable to allocate memory\n");
return -1;
}
arr = mem + cache_line_size - (long)mem % cache_line_size;
stop_addr = arr + mem_size;
for (iter = 0 ; iter < iter_count; ++iter) {
start_clock = clock();
for (ix_char = 0 ; ix_char < cache_line_size ; ++ix_char) {
for ( ix_line = arr; ix_line < stop_addr ; ix_line += cache_line_size) {
*(ix_line + ix_char) = c;
}
}
diff_ms = (clock() - start_clock) / CLOCK_IN_MS;
printf("iter=%d unseq time=%lu\n", iter, diff_ms);
}
free(mem);
return 0;
}
Среднее время: 5188 ms
iter=0 unseq time=5521
iter=1 unseq time=5122
iter=2 unseq time=5110
iter=3 unseq time=5160
iter=4 unseq time=5130
iter=5 unseq time=5124
iter=6 unseq time=5170
iter=7 unseq time=5181
iter=8 unseq time=5195
iter=9 unseq time=5163
Среднее время оптимизированной версии: 5735 ms
iter=0 unseq time=6067
iter=1 unseq time=5694
iter=2 unseq time=5704
iter=3 unseq time=5695
iter=4 unseq time=5692
iter=5 unseq time=5695
iter=6 unseq time=5707
iter=7 unseq time=5698
iter=8 unseq time=5704
iter=9 unseq time=5691
Непоследовательный доступ в C#. Массив в куче
using System.Diagnostics;
const int typicalItarationsCount = 10;
const int arraySize = 1073741824;
const int lineLength = 64;
const int linesCount = arraySize / lineLength;
var tmpArray = new bool[arraySize];
for(var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
var watch = new Stopwatch();
watch.Start();
for(long i = 0; i < lineLength; ++i)
{
var currentLineStart = 0;
for(long j = 0; j < linesCount; ++j)
{
tmpArray[currentLineStart + i] = true;
currentLineStart += lineLength;
}
}
watch.Stop();
Console.WriteLine($"iter={iteration} seq time={watch.ElapsedMilliseconds}");
}
Результаты:
Среднее время: 5647 ms
iter=0 seq time=5969
iter=1 seq time=5637
iter=2 seq time=5568
iter=3 seq time=5618
iter=4 seq time=5568
iter=5 seq time=5617
iter=6 seq time=5623
iter=7 seq time=5637
iter=8 seq time=5626
iter=9 seq time=5608
Непоследовательный доступ в C#. Неуправляемая память
using System.Diagnostics;
using System.Runtime.InteropServices;
unsafe
{
const int typicalItarationsCount = 10;
const int arraySize = 1073741824;
const int lineLength = 64;
const int linesCount = arraySize / lineLength;
for(var iteration = 0; iteration < typicalItarationsCount; ++iteration)
{
bool* buffer = (bool*)NativeMemory.Alloc((nuint) arraySize, sizeof(bool));
var readPtr = buffer;
var endPtr = buffer + arraySize;
var watch = new Stopwatch();
watch.Start();
for(long i = 0; i < lineLength; ++i)
{
readPtr = buffer + i;
for(long j = 0; j < linesCount; ++j)
{
*readPtr = true;
readPtr += lineLength;
}
}
watch.Stop();
NativeMemory.Free(buffer);
Console.WriteLine($"iter={iteration} seq time={watch.ElapsedMilliseconds}");
}
}
Результаты:
Среднее время: 6145 ms
iter=0 seq time=6166
iter=1 seq time=6160
iter=2 seq time=6142
iter=3 seq time=6135
iter=4 seq time=6152
iter=5 seq time=6130
iter=6 seq time=6120
iter=7 seq time=6160
iter=8 seq time=6138
iter=9 seq time=6142
Для тестов специально были выбраны такие реализации программ, чтобы разница арифметических операциях не влияла на время исполнения.
P.S.: Это мой первый опыт написания подобных статей, не судите строго за шероховатости.
Комментарии (46)
Vad344
04.11.2024 19:43В случае C это будет последовательный блок неуправляяемой помяти, полученный с помощью malloc, а в случае C# мы рассмотрим как блок памяти находящийся в управляемой куче...
Т.е., фактически, сравниваете скорость выделения памяти операнционной системы со скоростью встроенного диспетчера памяти среды выполнения C#? :)
Smerig
04.11.2024 19:43а если учесть, что диспетчер тоже обращается к ОС, чтоб та выделила память, то еще интереснее получается: просто смотрим на скорость быстродействия диспетчера в вакууме.
Kotofay
04.11.2024 19:43Отличный тест, "точный, как швейцарские часы":
for(var iteration = 0; iteration < typicalItarationsCount; ++iteration) { . . . tmpArray = new bool[arraySize]; . }
pr_ophet Автор
04.11.2024 19:43В замерах выделение памяти не участвует, gc на время выполнения не повлияет, так как он соберет большой объект только при выделении памяти, когда память выделенная для кучи закончится. Соберет он его вместе с поколением 2. Именно для того, чтобы сборка мусора не произошла в неудобный момент, выделений памяти вынесено за пределы измеряемого блока кода. Пробовал с ручной сборкой LOH, результаты не отличались. Оставил так для простоты кода.
Kotofay
04.11.2024 19:43Два простейших кода на Java и C показывают, что не всё так однозначно:
Kelbon
04.11.2024 19:43конечно заполнение массива интересное, сверху (int)ptr, что вообще непонятно какое число даёт (unspecified + overflow)
внизу в джаве тоже с оверфлоу заполнениеЕсли пойти посмотреть на генерируемый код, то (ожидаемо) подобные конструкции полностью исчезают, т.к. ничего не делают
https://godbolt.org/z/M6W43GG4EKotofay
04.11.2024 19:43Чтобы умный gcc не выбрасывал код достаточно сделать указатель статическим и присваивать не 1 а что то изменяющееся, например "i & 0xFF"
Kelbon
04.11.2024 19:43это называется shadowing. Ошибки нет
https://godbolt.org/z/jPf33Wz3rвсё по вашим советам, ничего не изменилось
Kotofay
04.11.2024 19:43И можно посмотреть. Как видим, в строке 9 происходит усечение значения из 64 в 8 бит без каких либо проблем, точно так же в моём коде происходит усечение значения адреса.
Код, в данном случае, не выбрасывается оптимизатором потому что указатель m передаётся в ф-ю free, и может быть использован в другой единице компиляции. По крайней мере он должен так делать. Если не делает, то оптимизатор ещё надо дорабатывать.
Kelbon
04.11.2024 19:43так вы сделали нечто очень странное, написали функцию, которую вызывать дважды == UB.
У вас просто очень странный код(которого не будет в реальности никогда), поэтому компилятор не оптимизировал
Компилятор знает что делает функция free, это по стандарту определено, поэтому компилятор может оптимизироватьKotofay
04.11.2024 19:43У вас просто очень странный код(которого не будет в реальности никогда), поэтому компилятор не оптимизировал
Это было сделано только для целей отключения оптимизации затенения(shadowing).
Таких нереентрантных ф-й в стандартной библиотеке в достатке, к примеру: char *strtok(char *string, const char *delim);
Первый вызов - первый аргумент строка, второй вызов -- первый аргумент может быть NULL. И где по вашему сохраняется указатель на строку? Это первое.
Второе -- MSVC при таком коде выдаёт ошибку компиляции, и это правильно, т.к. статическая переменная внутри ф-ии инициализируется один раз. Очень странно, что godbolt это компилирует без ошибок.
N.B. Для тех кто не писал на ассемблере, весь С "== UB".
alexander_kuznetsov
04.11.2024 19:43На будущее, однозначно рекомендую использовать вместо среднего медиану. Она как раз не чувствительна к выбросам, по типу тех, что бывали у вас в начале.
viruseg
04.11.2024 19:43Компилятор 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 чтобы проверить разницу с шарпом. Оставляю это для вас.
VBDUnit
04.11.2024 19:43В новых .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# должны уже показывать +‑ одинаковые результаты. Просто потому что это подразумевает отказ почти ото всех абстракций, которые дают языки, кроме каких‑то базовых. Дальше уже только ассемблер. Который в разном виде можно так или иначе запустить во всех языках.
viruseg
04.11.2024 19:43Не стал упоминать Vector512 иначе бы пришлось этот маленький пример превратить в простыню из:
if (Vector512.IsHardwareAccelerated) {} else if (Vector256.IsHardwareAccelerated) {} else if (Vector128.IsHardwareAccelerated) {} else if (Vector64.IsHardwareAccelerated) {} else {}
Но это всё не имеет значения в контексте этого теста. Да и моё железо не поддерживает Vector512.
pr_ophet Автор
04.11.2024 19:43Вроде бы, нет тут 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 ускориться.viruseg
04.11.2024 19:43Отметил на скрине avx инструкции.
Можно здесь посмотреть. https://sharplab.io/
Ваш код проверил, цифры те же +- что и у обычного массива.
Насчёт этого не понял. Этот код с Vector256 ну просто не может быть по скорости таким же как как ваш из статьи.
upd: не заметил, что вы написали в комментарии скомпилированный код C, а не c#. На C я не работал. Поэтому подумал, что там есть SIMD инструкции после компиляции и не проверил свою гипотезу.
Cheater
04.11.2024 19:43.file "test.c"
- речь шла про сишный ассемблер.Ответ для C - думаю потому что компилятор решил сгенерировать вызов
memset
и все SIMD оптимизации идут уже в нём.viruseg
04.11.2024 19:43Похоже что так и есть. Chatgpt говорит, что:
memset
относится к языку C как часть стандартной библиотеки, но её оптимизация зависит от компилятора и библиотеки, связанной с ОС.Без SIMD такой буст как в тестах в статье просто невозможен.
LittleAlien
04.11.2024 19:43Gcc всё сводит к вызову memset (call memset@PLT), видимо у неё внутри SIMD есть.
Можно Clang смотреть, там SIMD в явном виде (строки 72-74): https://gcc.godbolt.org/z/PPPz6xv6s
Kelbon
04.11.2024 19:43т.е. тестировался вывод в консоль (prinf внутри цикла "бенчмарка") и в статье про заполнение памяти не упомянуты memset и memcpy, а всё происходило внутри функции main, которая для гцц особая и оптимизируется иначе.
Что ж, эталонный бенчмарк
NikkiG
04.11.2024 19:43Вы код читали? Вывод и заполнение памяти вне измеряемого цикла же.
Kelbon
04.11.2024 19:43ну раз вы читали, то может вы расскажете почему компилятору нельзя просто удалить этот код заполнения памяти, если она нигде не используется
NikkiG
04.11.2024 19:43Вы многого хотите от компилятора, чтоб он оценивал используемость памяти в куче)
Это уже автоматическое управление памятью будет)
Kelbon
04.11.2024 19:43Вы переоцениваете сложность задачи
https://godbolt.org/z/Pad8Mc3ca
Как видно, никаких манипуляций с памятью, в том числе её выделения, нет
pr_ophet Автор
04.11.2024 19:43Речь идет о C, а не о C++. Проверьте вышеприведенный код с этим языком
Kelbon
04.11.2024 19:43компилируется тем же компилятором, по практически тем же правилам, что там может измениться?
https://godbolt.org/z/G69hdzEdc
Shersh
04.11.2024 19:43Пожалуйста не используйте эти стопаотчи для тестирования производительности.
Для бенчмарков в .net есть уже либо - BenchmarkDotnet ей и пользуйтесь
rutsh
04.11.2024 19:43Для "C" самым шустрым на данный момент является всё же не gcc-13, а clang-18.
Конкретно в случае моей машины получаем "avg seq time=309.10" на gcc против "avg seq time=134.50" на clang в случае последовательной записи в память
viruseg
04.11.2024 19:43Вспомнил, что в c# есть Span<T>.Fill. Для C использовал компилятор clang-19.1.0 с аргументом "-O3". Результат, абсолютно одинаковая скорость. Так что выводы в статье абсолютно неверные, как и код для тестирования.
viordash
для чистоты эксперимента в первом тесте, я бы посоветовал вам в коде для "c" переместить выделение\освобождение памяти в цикл
for (iter = 0 ; iter < iter_count; ++iter)
, наподобии кода в c#.viordash
и чтобы для си компилятор не выкинул заполнение памяти, добавьте volatile
volatile const char c = 1;
aamonster
Может, сразу sleep(1) в цикл добавить для надёжности? :-)
Лучше уж код на C# нормально написать, не создавая 100500 массивов в цикле.
viordash
ну только если вам это нужно для теста