У меня немного странное поведение с методом, который я сделал, когда я пытаюсь проверить его производительность, в основном, если я закомментирую / отключу один из возвратов в одном из операторов if, он перейдет с 400 мс на 4 мс, почти как он компилируется и на самом деле не запускает код, это имело бы смысл, если бы после комментирования / отключения одного возврата он был только return true или false, поэтому у него был только один вариант, тогда я могу увидеть, как компилятор оптимизирует его и всегда устанавливайте его как bool, а не запускайте код.
Кто-нибудь знает, что может происходить, или есть рекомендации по лучшему способу запуска теста?
Мой тестовый код:
Vec3 spherePos = new Vec3(43.7527, 75.9756, 0);
double sphereRadisSq = 50 * 50;
Vec3 rayPos = new Vec3(-5.32301, 5.97157, -112.983);
Vec3 rayDir = new Vec3(0.457841, 0.680324, 0.572312);
sw.Reset();
sw.Start();
bool res = false;
for (int i = 0; i < 10000000; i++)
{
res = Intersect.RaySphereFast(rayPos, rayDir, spherePos, sphereRadisSq);
}
sw.Stop();
Debug.Log($"testTime: {sw.ElapsedMilliseconds} ms");
Debug.Log(res);
И статический метод:
public static bool RaySphereFast(Vec3 _rp, Vec3 _rd, Vec3 _sp, double _srsq)
{
double rs = Vec3.DistanceFast(_rp, _sp);
if (rs < _srsq)
{
return (true); // <-- When I disable this one
}
Vec3 p = Vec3.ProjectFast(_sp, _rp, _rd);
double pr = Vec3.Dot(_rd, (p - _rp));
if (pr < 0)
{
return (false); // <-- Or when I disable this one
}
double ps = Vec3.DistanceFast(p, _sp);
if (ps < _srsq)
{
return (true); // <-- Or when I disable this one
}
return (false);
}
Структура Vec3 (похудел):
public struct Vec3
{
public Vec3(double _x, double _y, double _z)
{
x = _x;
y = _y;
z = _z;
}
public double x { get; }
public double y { get; }
public double z { get; }
public static double DistanceFast(Vec3 _v0, Vec3 _v1)
{
double x = (_v1.x - _v0.x);
double y = (_v1.y - _v0.y);
double z = (_v1.z - _v0.z);
return ((x * x) + (y * y) + (z * z));
}
public static double Dot(Vec3 _v0, Vec3 _v1)
{
return ((_v0.x * _v1.x) + (_v0.y * _v1.y) + (_v0.z * _v1.z));
}
public static Vec3 ProjectFast(Vec3 _p, Vec3 _a, Vec3 _d)
{
Vec3 ap = _p - _a;
return (_a + Vec3.Dot(ap, _d) * _d);
}
public static Vec3 operator +(Vec3 _v0, Vec3 _v1)
{
return (new Vec3(_v0.x + _v1.x, _v0.y + _v1.y, _v0.z + _v1.z));
}
public static Vec3 operator -(Vec3 _v0, Vec3 _v1)
{
return new Vec3(_v0.x - _v1.x, _v0.y - _v1.y, _v0.z - _v1.z);
}
public static Vec3 operator *(double _d1, Vec3 _v0)
{
return new Vec3(_d1 * _v0.x, _d1 * _v0.y, _d1 * _v0.z);
}
}
@ChetanRanpariya - любой из трех, содержащихся в другом операторе if.
Это имеет смысл, поскольку во всех случаях он может оптимизировать кучу кода и, вероятно, так и делает, хотя в 100 раз быстрее не кажется правильным
Было бы проще, если бы мы увидели реализацию Vec3
. Затем мы могли бы посмотреть на сгенерированный IL
@TheGeneral, когда я увеличиваю цикл еще в 10 раз, он увеличивается с ~ 4000 мс до ~ 28 мс, это точно не может быть правильным.
Вы должны проанализировать разницу кода IL, созданного двумя версиями кода. Оптимизация компилятора иногда делает чудеса.
@FCin Добавил в сообщение структуру Vec3,
Использование профилировщика производительности - всегда хорошая идея.
@UweKeim Я компилирую в режиме выпуска, тогда профиль мне мало что говорит, в режиме отладки я не вижу такой большой разницы.
Я создал новую форму выигрыша с этим кодом только в нем, и когда я попытался запустить ее, это было около 500 мс как с включенным, так и с отключенным возвратом, но для проекта было установлено значение .Net 3.5, поэтому я изменил его на 4.6.1, как и в другом моем project, а затем он снова пошел на 4 мс, сейчас я пытаюсь изучить материал кода IL.
Я не могу воспроизвести переход от 400 мс до 4 мс, но после того, как я прокомментирую любой if
, я получаю от 800 мс до 200-300 мс. Profiler показывает, что большая часть работы выполняется в Vec3.ProjectFast
, который рассчитан для ps
и pr
. Посмотрев на IL, я не вижу ничего необычного. Единственная оптимизация, которую можно сделать при комментировании if
, заключается в том, что ps
или pr
не нужно вычислять, но они занимают только ~ 10% всего времени вычислений. Вы уверены, что комментируя if
, получаете 4 мс?
Вероятно, это происходит из-за того, что при комментировании возвращаемых результатов сложность метода падает ниже порога, при котором автоматическое встраивание отключается.
Это встраивание не видно в сгенерированном IL - это выполняется JIT-компилятором.
Мы можем проверить эту гипотезу, украсив рассматриваемый метод атрибутом [MethodImpl(MethodImplOptions.AggressiveInlining)]
.
Когда я попробовал это с вашим кодом, я получил следующие результаты (выпуск, сборка x64):
Original code: 302 ms
First return commented out: 2 ms
Decorated with AggressiveInlining: 2 ms
Время с закомментированным первым возвратом такое же, как и при декорировании метода с помощью AggressiveInlining
(оставив первый возврат включенным).
Поэтому я прихожу к выводу, что гипотеза верна.
Просто чтобы добавить (очевидный) отказ от ответственности к ответу @Matthew Watson
Результаты зависят от версии .NET, версии JIT и т. д. К вашему сведению, я не могу воспроизвести такую разницу, и результаты в моей среде практически одинаковы.
Я использую BenchmarkDotNet с .NET Core 2.1.0, подробности см. Ниже
// * Summary *
BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.228 (1803/April2018Update/Redstone4)
Intel Core i7-4700MQ CPU 2.40GHz (Max: 1.08GHz) (Haswell), 1 CPU, 8 logical and 4 physical cores
Frequency=2338346 Hz, Resolution=427.6527 ns, Timer=TSC
.NET Core SDK=2.2.100-preview1-009349
[Host] : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
DefaultJob : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
Method | Mean | Error | StdDev |
----------------------- |---------:|----------:|----------:|
RaySphereFast_Original | 40.06 ns | 0.3693 ns | 0.3455 ns |
RaySphereFast_NoReturn | 40.46 ns | 0.0860 ns | 0.0805 ns |
// * Legends *
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
1 ns : 1 Nanosecond (0.000000001 sec)
// ***** BenchmarkRunner: End *****
Run time: 00:00:34 (34.86 sec), executed benchmarks: 2
// * Artifacts cleanup *
Здесь происходит несколько интересных вещей. Как отмечали другие, когда вы комментируете один из возвращаемых результатов, метод RaySphereFast
теперь становится достаточно маленьким для встраивания, и действительно, jit решает встроить его. А это, в свою очередь, встраивает все вызываемые вспомогательные методы. В результате тело цикла остается без вызовов.
Как только это произойдет, jit затем «struct продвигает» различные экземпляры Vec3
, и, поскольку вы инициализировали все поля константами, jit распространяет эти константы и сворачивает их при различных операциях. Из-за этого jit понимает, что результатом вызова всегда будет true
.
Поскольку каждая итерация цикла возвращает одно и то же значение, jit понимает, что ни одно из этих вычислений в цикле на самом деле не является необходимым (поскольку результат известен), и удаляет их все. Итак, в «быстрой» версии вы синхронизируете пустой цикл:
G_M52940_IG04:
BF01000000 mov edi, 1
FFC1 inc ecx
81F980969800 cmp ecx, 0x989680
7CF1 jl SHORT G_M52940_IG04
в то время как в "медленной" версии вызов не вставляется, и никакая из этой оптимизации не срабатывает:
G_M32193_IG04:
488D4C2478 lea rcx, bword ptr [rsp+78H]
C4617B1109 vmovsd qword ptr [rcx], xmm9
C4617B115108 vmovsd qword ptr [rcx+8], xmm10
C4617B115910 vmovsd qword ptr [rcx+16], xmm11
488D4C2460 lea rcx, bword ptr [rsp+60H]
C4617B1121 vmovsd qword ptr [rcx], xmm12
C4617B116908 vmovsd qword ptr [rcx+8], xmm13
C4617B117110 vmovsd qword ptr [rcx+16], xmm14
488D4C2448 lea rcx, bword ptr [rsp+48H]
C4E17B1131 vmovsd qword ptr [rcx], xmm6
C4E17B117908 vmovsd qword ptr [rcx+8], xmm7
C4617B114110 vmovsd qword ptr [rcx+16], xmm8
488D4C2478 lea rcx, bword ptr [rsp+78H]
488D542460 lea rdx, bword ptr [rsp+60H]
4C8D442448 lea r8, bword ptr [rsp+48H]
C4E17B101D67010000 vmovsd xmm3, qword ptr [reloc @RWD64]
E8D2F8FFFF call X:RaySphereFast(struct,struct,struct,double):bool
8BD8 mov ebx, eax
FFC7 inc edi
81FF80969800 cmp edi, 0x989680
7C95 jl SHORT G_M32193_IG04
Если вы действительно заинтересованы в тестировании скорости RaySphereFast
, убедитесь, что вы вызываете его с разными или непостоянными аргументами на каждой итерации, а также убедитесь, что вы используете результат каждой итерации.
Какой ответ вы комментируете?