Почему установщики свойств индексатора не встроены?

#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. Вы просто не измеряете, кем вы себя считаете. Чтение памяти, которое никогда не используется, не имеет побочных эффектов, поэтому может быть устранено. Запись в память невозможна, что может повлиять на код, выполняемый в других потоках. Или, другими словами, оптимизатор дрожания обрабатывает записи указателя как изменчивые. Вам понадобится более реалистичное использование вашего класса, чтобы измерить его реальную выгоду.