Почему локальный массив быстрее, чем статический для чтения/записи?
Я писал несколько тестов бенчмаркинга, чтобы выяснить, почему подобный чистый алгоритм (не С++ lib/.net, построенный в классах) работал быстрее на С++, чем на С#, даже если учитывал ожидаемые отличия функций. И, делая это, я наткнулся на эти 2 теста, которые сбивали меня с толку, есть ли у кого-нибудь представление о том, почему он существенно медленнее, чем другой? Единственное отличие во втором (что занимает 51 мс против 88 на моей машине) состоит в том, что 2 массива объявляются локально в методе вместо внешнего. В обоих случаях массивы создаются до начала отсчета времени.
const int Runs = 100;
const int Width = 5000;
const int Height = 5000;
const int Size = Width * Height;
static int[] Input = Enumerable.Range(0, Size).ToArray();
static int[] Output = new int[Size * 2];
static int SimpleTest()
{
// Removing those 2 lines and using the static arrays instead give substantially slower performance, nearly half the speed!
int[] Input = Enumerable.Range(0, Size).ToArray();
int[] Output = new int[Size * 2];
Stopwatch sw = new Stopwatch();
sw.Start();
for (int run = 0; run < Runs; run++)
{
int InputIndex = 0;
for (int x = 0; x < Width; x++)
{
for (int y = 0; y < Height; y++)
{
int pixel = Input[InputIndex];
var OutputIndex = InputIndex * 2;
Output[OutputIndex] = pixel;
Output[OutputIndex + 1] = pixel;
InputIndex++;
}
}
}
sw.Stop();
return (int)(sw.ElapsedMilliseconds / Runs);
}
Ответы
Ответ 1
Когда переменные являются локальными, компилятор знает, что Input
и Output
никогда не изменятся, что открывает множество оптимизаций.
- Значение переменных
Input
и Output
может храниться в регистрах.
-
Input.Length
и Output.Length
можно вычислить один раз и кэшировать.
- Компилятор может доказать, что
Input[InputIndex]
и Output[OutputIndex]
никогда не приведут к тому, что индекс массива окажется за пределами границ, поэтому проверка границ может быть оптимизирована.
- Компилятор может заметить, что результаты
Input
и Output
никогда не используются, поэтому он может оптимизировать петли до нуля!
Если вы используете статические версии, компилятор не может выполнить эти оптимизации. Компилятор должен перезагрузить Input
и Output
при каждом доступе и должен выполнить проверку границ в каждой операции индекса массива, на всякий случай, если другой поток изменен Input
или Output
.
Например, если другой поток выполняет Input = new int[Size]
, тогда все будущие вычисления должны начинаться с этого альтернативного Input
. И если другой поток имеет Output = new int[1]
, тогда код должен поднять IndexOutOfRangeException
.
Ответ 2
С 32-битным JIT я считаю, что виновником является, как заметил Раймонд Чен, что вход и выход можно хранить в регистрах, когда они являются местными, но их нужно перезагружать каждый раз, когда они не являются. Сгенерированная сборка:
Для локальных жителей:
007426F0 mov eax,dword ptr [ebp-18h]
007426F3 mov edi,dword ptr [eax+4]
int pixel = Input[InputIndex];
007426F6 mov eax,dword ptr [ebp-18h]
007426F9 cmp edx,edi
007426FB jae 0074276E
007426FD mov ecx,dword ptr [eax+edx*4+8]
Для статики:
011C2718 mov dword ptr [ebp-18h],edx
011C271B mov esi,dword ptr ds:[3BB7E90h]
011C2721 mov eax,dword ptr [esi+4]
011C2724 mov dword ptr [ebp-1Ch],eax
int pixel = Input[InputIndex];
011C2727 mov eax,dword ptr [ebp-1Ch]
011C272A cmp ecx,eax
011C272C jae 011C27A2
011C272E mov edi,dword ptr [esi+ecx*4+8]
Как вы можете видеть, mov esi,dword ptr ds:[3BB7E90h]
обращается к сегменту данных.
Как вы можете видеть, проверка границ происходит в обоих случаях (cmp-jae
), так что не имеет значения, и петли на самом деле не оптимизированы ни к чему.
Как 64-разрядный JIT избегает этой проблемы, находится вне меня.
Здесь полная разборка для обоих случаев:
Быстрая версия:
for (int x = 0; x < Width; x++) {
007426EB mov dword ptr [ebp-14h],edx
for (int y = 0; y < Height; y++) {
007426EE xor ebx,ebx
007426F0 mov eax,dword ptr [ebp-18h]
007426F3 mov edi,dword ptr [eax+4]
int pixel = Input[InputIndex];
007426F6 mov eax,dword ptr [ebp-18h]
007426F9 cmp edx,edi
007426FB jae 0074276E
007426FD mov ecx,dword ptr [eax+edx*4+8]
var OutputIndex = InputIndex * 2;
00742701 mov esi,edx
00742703 add esi,esi
Output[OutputIndex] = pixel;
00742705 mov eax,dword ptr [ebp-1Ch]
00742708 cmp esi,dword ptr [eax+4]
0074270B jae 0074276E
0074270D mov dword ptr [eax+esi*4+8],ecx
Output[OutputIndex + 1] = pixel;
00742711 inc esi
00742712 mov eax,dword ptr [ebp-1Ch]
00742715 cmp esi,dword ptr [eax+4]
00742718 jae 0074276E
0074271A mov dword ptr [eax+esi*4+8],ecx
InputIndex++;
0074271E inc edx
for (int y = 0; y < Height; y++) {
0074271F inc ebx
for (int y = 0; y < Height; y++) {
00742720 cmp ebx,1388h
00742726 jl 007426F6
for (int x = 0; x < Width; x++) {
00742728 inc dword ptr [ebp-14h]
0074272B cmp dword ptr [ebp-14h],1388h
00742732 jl 007426EE
Медленная версия:
for (int x = 0; x < Width; x++) {
011C2713 mov dword ptr [ebp-14h],ecx
for (int y = 0; y < Height; y++) {
011C2716 xor edx,edx
011C2718 mov dword ptr [ebp-18h],edx
011C271B mov esi,dword ptr ds:[3BB7E90h]
011C2721 mov eax,dword ptr [esi+4]
011C2724 mov dword ptr [ebp-1Ch],eax
int pixel = Input[InputIndex];
011C2727 mov eax,dword ptr [ebp-1Ch]
011C272A cmp ecx,eax
011C272C jae 011C27A2
011C272E mov edi,dword ptr [esi+ecx*4+8]
var OutputIndex = InputIndex * 2;
011C2732 mov ebx,ecx
011C2734 add ebx,ebx
Output[OutputIndex] = pixel;
011C2736 mov edx,dword ptr ds:[3BB7E94h]
011C273C cmp ebx,dword ptr [edx+4]
011C273F jae 011C27A2
011C2741 mov dword ptr [edx+ebx*4+8],edi
Output[OutputIndex + 1] = pixel;
011C2745 inc ebx
011C2746 cmp ebx,dword ptr [edx+4]
011C2749 jae 011C27A2
011C274B mov dword ptr [edx+ebx*4+8],edi
InputIndex++;
011C274F inc ecx
for (int y = 0; y < Height; y++) {
011C2750 inc dword ptr [ebp-18h]
011C2753 cmp dword ptr [ebp-18h],1388h
011C275A jl 011C2727
for (int x = 0; x < Width; x++) {
011C275C inc dword ptr [ebp-14h]
011C275F cmp dword ptr [ebp-14h],1388h
011C2766 jl 011C2716
Ответ 3
Интересно, может ли это быть похоже на разницу в производительности между статическими и функциями-членами? Вызов статических методов не проверяется с нулевым значением, тогда как вызовы функций экземпляра имеют нулевое значение.
Кроме того, я вижу разные результаты, чем вы. Статические массивы имеют более короткое время работы на моей машине. 64.x мс за ход против около 75.x мс.
Вот полная программа, которую я использовал. Выполняется на Mono С#, на OSX.
using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Time per run was: " + SimpleTest());
}
const int Runs = 10;
const int Width = 5000;
const int Height = 5000;
const int Size = Width * Height;
static int[] Input = Enumerable.Range(0, Size).ToArray();
static int[] Output = new int[Size * 2];
static float SimpleTest()
{
// Removing those 2 lines and using the static arrays instead give substantially slower performance, nearly half the speed!
//int[] Input = Enumerable.Range(0, Size).ToArray();
//int[] Output = new int[Size * 2];
Stopwatch sw = new Stopwatch();
sw.Start();
for (int run = 0; run < Runs; run++)
{
int InputIndex = 0;
for (int x = 0; x < Width; x++)
{
for (int y = 0; y < Height; y++)
{
int pixel = Input[InputIndex];
var OutputIndex = InputIndex * 2;
Output[OutputIndex] = pixel;
Output[OutputIndex + 1] = pixel;
InputIndex++;
}
}
}
sw.Stop();
return (sw.ElapsedMilliseconds / (float)Runs);
}
}
}