Границы массива проверяют оптимизацию в цикле for
var ar = new int[500000000];
var sw = new Stopwatch();
sw.Start();
var length = ar.Length;
for (var i = 0; i < length; i++)
{
if (ar[i] == 0);
}
sw.Stop();
sw.ElapsedMilliseconds: ~2930 мс
var ar = new int[500000000];
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < ar.Length; i++)
{
if (ar[i] == 0);
}
sw.Stop();
sw.ElapsedMilliseconds: ~3520мс
Win8x64, VS12, .NET4.5, выпуск сборки, "Оптимизация кода" включена.
Насколько я знаю, второй подход должен быть быстрее из-за оптимизации проверки границ массива. Я что-то пропустил?
4 ответа
Я также использую Win8 x64, .NET 4.5, Release build вне отладчика (это важно); Я получил:
0: 813ms vs 421ms
1: 439ms vs 420ms
2: 440ms vs 420ms
3: 431ms vs 429ms
4: 433ms vs 427ms
5: 424ms vs 437ms
6: 427ms vs 434ms
7: 430ms vs 432ms
8: 432ms vs 435ms
9: 430ms vs 430ms
10: 427ms vs 418ms
11: 422ms vs 421ms
12: 434ms vs 420ms
13: 439ms vs 425ms
14: 426ms vs 429ms
15: 426ms vs 426ms
16: 417ms vs 432ms
17: 442ms vs 425ms
18: 420ms vs 429ms
19: 420ms vs 422ms
Первый платит JIT / стоимость "слияния", но в целом она примерно одинакова (некоторые в каждом столбце выглядят быстрее, но в целом не о чем говорить).
using System;
using System.Diagnostics;
static class Program
{
static void Main()
{
var ar = new int[500000000];
for (int j = 0; j < 20; j++)
{
var sw = Stopwatch.StartNew();
var length = ar.Length;
for (var i = 0; i < length; i++)
{
if (ar[i] == 0) ;
}
sw.Stop();
long hoisted = sw.ElapsedMilliseconds;
sw = Stopwatch.StartNew();
for (var i = 0; i < ar.Length; i++)
{
if (ar[i] == 0) ;
}
sw.Stop();
long direct = sw.ElapsedMilliseconds;
Console.WriteLine("{0}: {1}ms vs {2}ms", j, hoisted, direct);
}
}
}
Я исследовал это еще немного и обнаружил, что действительно трудно сделать тест, который фактически показывает эффект оптимизации исключения проверки границ.
Сначала некоторые проблемы со старым тестом:
- Разборка показала, что JIT-компилятор смог оптимизировать и первую версию. Это было для меня неожиданностью, но разборка не лжет. Это, конечно, полностью противоречит цели этого теста. Исправлено: взять длину в качестве аргумента функции.
- Массив слишком большой, что означает отсутствие кеша, что добавляет много шума нашему сигналу. Исправлено: использовать короткий массив, но повторять его несколько раз.
Но теперь настоящая проблема: он делает что-то слишком умное. Во внутреннем цикле нет проверки границ массива, даже если длина цикла определяется аргументом функции. Сгенерированный код отличается, но внутренний цикл по сути тот же. Не полностью (разные регистры и т. Д.), Но по той же схеме:
_loop: mov eax, [somewhere + index]
add index, 4
cmp index, end
jl _loop
Нет существенной разницы во времени выполнения, потому что нет существенной разницы в той части сгенерированного кода, которая наиболее важна.
Я думаю, что ответ заключается в том, что сборщик мусора работает и меняет время.
Отказ от ответственности: я не могу увидеть весь контекст кода OP, потому что вы не опубликовали скомпилированный пример; Я предполагаю, что вы перераспределяете массив, а не используете его повторно. Если нет, то это не правильный ответ!
Рассмотрим этот код:
using System;
using System.Diagnostics;
namespace Demo
{
internal class Program
{
private static void Main(string[] args)
{
var ar = new int[500000000];
test1(ar);
//ar = new int[500000000]; // Uncomment this line.
test2(ar);
}
private static void test1(int[] ar)
{
var sw = new Stopwatch();
sw.Start();
var length = ar.Length;
for (var i = 0; i < length; i++)
{
if (ar[i] == 0);
}
sw.Stop();
Console.WriteLine("test1 took " + sw.Elapsed);
}
private static void test2(int[] ar)
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < ar.Length; i++)
{
if (ar[i] == 0);
}
sw.Stop();
Console.WriteLine("test2 took " + sw.Elapsed);
}
}
}
В моей системе это печатает:
test1 took 00:00:00.6643788
test2 took 00:00:00.3516378
Если я раскомментирую строку, отмеченную // Uncomment this line.
тогда сроки изменяются на:
test1 took 00:00:00.6615819
test2 took 00:00:00.6806489
Это из-за того, что GC собирает предыдущий массив.
[EDIT] Чтобы избежать затрат на запуск JIT, я поместил весь тест в цикл:
for (int i = 0; i < 8; ++i)
{
test1(ar);
ar = new int[500000000]; // Uncomment this line.
test2(ar);
}
И тогда мои результаты с выделением второго массива закомментированы так:
test1 took 00:00:00.6437912
test2 took 00:00:00.3534027
test1 took 00:00:00.3401437
test2 took 00:00:00.3486296
test1 took 00:00:00.3470775
test2 took 00:00:00.3675475
test1 took 00:00:00.3501221
test2 took 00:00:00.3549338
test1 took 00:00:00.3427057
test2 took 00:00:00.3574063
test1 took 00:00:00.3566458
test2 took 00:00:00.3462722
test1 took 00:00:00.3430952
test2 took 00:00:00.3464017
test1 took 00:00:00.3449196
test2 took 00:00:00.3438316
И с включенным выделением второго массива:
test1 took 00:00:00.6572665
test2 took 00:00:00.6565778
test1 took 00:00:00.3576911
test2 took 00:00:00.6910897
test1 took 00:00:00.3464013
test2 took 00:00:00.6638542
test1 took 00:00:00.3548638
test2 took 00:00:00.6897472
test1 took 00:00:00.4464020
test2 took 00:00:00.7739877
test1 took 00:00:00.3835624
test2 took 00:00:00.8432918
test1 took 00:00:00.3496910
test2 took 00:00:00.6471341
test1 took 00:00:00.3486505
test2 took 00:00:00.6527160
Обратите внимание, что test2 постоянно занимает больше времени из-за GC.
К сожалению, GC делает результаты расчета времени бессмысленными.
Например, если я изменю код теста на этот:
for (int i = 0; i < 8; ++i)
{
var ar = new int[500000000];
GC.Collect();
test1(ar);
//ar = new int[500000000]; // Uncomment this line.
test2(ar);
}
С закомментированной строкой я получаю:
test1 took 00:00:00.6354278
test2 took 00:00:00.3464486
test1 took 00:00:00.6672933
test2 took 00:00:00.3413958
test1 took 00:00:00.6724916
test2 took 00:00:00.3530412
test1 took 00:00:00.6606178
test2 took 00:00:00.3413083
test1 took 00:00:00.6439316
test2 took 00:00:00.3404499
test1 took 00:00:00.6559153
test2 took 00:00:00.3413563
test1 took 00:00:00.6955377
test2 took 00:00:00.3364670
test1 took 00:00:00.6580798
test2 took 00:00:00.3378203
И это без комментариев:
test1 took 00:00:00.6340203
test2 took 00:00:00.6276153
test1 took 00:00:00.6813719
test2 took 00:00:00.6264782
test1 took 00:00:00.6927222
test2 took 00:00:00.6269447
test1 took 00:00:00.7010559
test2 took 00:00:00.6262000
test1 took 00:00:00.6975080
test2 took 00:00:00.6457846
test1 took 00:00:00.6796235
test2 took 00:00:00.6341214
test1 took 00:00:00.6823508
test2 took 00:00:00.6455403
test1 took 00:00:00.6856985
test2 took 00:00:00.6430923
Я думаю, что мораль этого теста такова: сборщик мусора для этого конкретного теста является настолько большим по сравнению с остальным кодом, что он полностью искажает результаты синхронизации, и им нельзя доверять, чтобы что-то значить.
Вы называете свойство на втором, так что это будет медленнее ar.Length