#c# #performance #generics #unmanaged
#c# #Производительность #общие #неуправляемый
Вопрос:
У меня есть класс, который переносит неуправляемое распределение в массив. Вы можете увидеть исходный код здесь, на Github, но вот его основная суть:
public unsafe class ArrayReference<T> : Reference, IArrayReference<T>
where T : unmanaged
{
private T* typedPointer_;
public T this[ int index ]
{
[MethodImpl( MethodImplOptions.AggressiveInlining )]
get => typedPointer_[ index ];
[MethodImpl( MethodImplOptions.AggressiveInlining )]
set => typedPointer_[ index ] = value;
}
}
Это довольно просто, и для операций чтения обеспечивает отличную производительность (измеряется с помощью BenchmarkDotNet):
Array size: 128
Managed byte[] ranged-for get: 69.8046 ns
ArrayReference ranged-for get: 66.7340 ns
Managed byte[] ranged-for set: 66.1855 ns
ArrayReference ranged-for set: 68.4863 ns
И тестовый код:
[GlobalSetup]
public void Setup()
{
median = AllocationSize / 2;
alloc_ = new Allocation( AllocationSize );
array_ = new ArrayReference<byte>( alloc_.Address, AllocationSize );
managedArray_ = new byte[ AllocationSize ];
}
[Benchmark]
public void ManagedArray_ranged_for_get()
{
var counter = 0;
for ( var i = 0; i < AllocationSize; i )
counter = managedArray_[ i ];
}
[Benchmark]
public void ArrayReference_ranged_for_get()
{
var counter = 0;
for ( var i = 0; i < AllocationSize; i )
counter = array_[ i ];
}
[Benchmark]
public void ManagedArray_ranged_for_set()
{
for ( var i = 0; i < AllocationSize; i )
managedArray_[ i ] = ( byte ) i;
}
[Benchmark]
public void ArrayReference_ranged_for_set()
{
for ( var i = 0; i < AllocationSize; i )
array_[ i ] = ( byte ) i;
}
Как вы можете видеть, чтение из ArrayReference
немного быстрее, потому что оно не выполняет проверку диапазона и имеет прямой доступ к указателю массива. Однако запись в ArrayReference
выполняется медленнее, чем в управляемом byte[]
массиве, и, похоже, проблема в том, что установщик не встроен.
JIT x86 для набора управляемых байтов[]:
managedArray_[ median ] = 0;
00007FFA237A216C mov rax,qword ptr [rbp 10h]
00007FFA237A2170 mov rax,qword ptr [rax 18h]
00007FFA237A2174 mov rdx,qword ptr [rbp 10h]
00007FFA237A2178 mov edx,dword ptr [rdx 24h]
00007FFA237A217B cmp rdx,qword ptr [rax 8]
00007FFA237A217F jb 00007FFA237A2186
00007FFA237A2181 call 00007FFA833DF110
00007FFA237A2186 lea rax,[rax rdx 10h]
00007FFA237A218B mov byte ptr [rax],0
JIT x86 для ArrayReference::set:
typedPointer_[ index ] = value;
00007FFA237A20BC mov rcx,qword ptr [rbp 10h]
00007FFA237A20C0 mov rcx,qword ptr [rcx 10h]
00007FFA237A20C4 mov rdx,qword ptr [rbp 10h]
00007FFA237A20C8 mov edx,dword ptr [rdx 24h]
00007FFA237A20CB xor r8d,r8d
00007FFA237A20CE cmp dword ptr [rcx],ecx
00007FFA237A20D0 call 00007FFA237A1738
->
00007FFA237A20F0 push rbp
00007FFA237A20F1 sub rsp,20h
00007FFA237A20F5 lea rbp,[rsp 20h]
00007FFA237A20FA mov qword ptr [rbp 10h],rcx
00007FFA237A20FE mov dword ptr [rbp 18h],edx
00007FFA237A2101 mov dword ptr [rbp 20h],r8d
00007FFA237A2105 cmp dword ptr [7FFA23688310h],0
00007FFA237A210C je 00007FFA237A2113
00007FFA237A210E call 00007FFA833DD3E0
00007FFA237A2113 mov rax,qword ptr [rbp 10h]
00007FFA237A2117 mov rax,qword ptr [rax 20h]
00007FFA237A211B mov edx,dword ptr [rbp 18h]
00007FFA237A211E movsxd rdx,edx
00007FFA237A2121 mov ecx,1
00007FFA237A2126 movsxd rcx,ecx
00007FFA237A2129 imul rdx,rcx
00007FFA237A212D mov ecx,dword ptr [rbp 20h]
00007FFA237A2130 mov byte ptr [rax rdx],cl
Я не понимаю, почему это не встроено. Он делает то же самое, что и управляемый массив, за исключением указателя на неуправляемую память. Нарушает ли это одно из правил встраивания CLR или, возможно, оно не встроено, потому что T
является общим, даже если оно ограничено?
Windows 10 Pro, 64-разрядная версия.Net Core 2.2, 64-разрядный RyuJIT в режиме выпуска
Комментарии:
1. Используйте Ildasm.exe чтобы получить IL и проанализировать это.
2. .NET Core или Framework? Какая версия? Отладка или выпуск сборки? На какой ОС вы работаете?
3. Действительно, во все эти цифры трудно поверить. При частоте 4 ГГц один цикл составляет 0,25 нс, и хотя современные процессоры могут выполнять несколько операций за один цикл, 20 операций get за цикл нереальны.
4. Покажите нам свой код синхронизации.
5. Вы просто не измеряете, кем вы себя считаете. Чтение памяти, которое никогда не используется, не имеет побочных эффектов, поэтому может быть устранено. Запись в память невозможна, что может повлиять на код, выполняемый в других потоках. Или, другими словами, оптимизатор дрожания обрабатывает записи указателя как изменчивые. Вам понадобится более реалистичное использование вашего класса, чтобы измерить его реальную выгоду.