Метод C# в 100 раз медленнее с тремя возвратами против двух?

У меня немного странное поведение с методом, который я сделал, когда я пытаюсь проверить его производительность, в основном, если я закомментирую / отключу один из возвратов в одном из операторов 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);
    }
}

Какой ответ вы комментируете?

Chetan 09.09.2018 08:40

@ChetanRanpariya - любой из трех, содержащихся в другом операторе if.

Patrik Fröhler 09.09.2018 08:41

Это имеет смысл, поскольку во всех случаях он может оптимизировать кучу кода и, вероятно, так и делает, хотя в 100 раз быстрее не кажется правильным

TheGeneral 09.09.2018 08:47

Было бы проще, если бы мы увидели реализацию Vec3. Затем мы могли бы посмотреть на сгенерированный IL

FCin 09.09.2018 08:51

@TheGeneral, когда я увеличиваю цикл еще в 10 раз, он увеличивается с ~ 4000 мс до ~ 28 мс, это точно не может быть правильным.

Patrik Fröhler 09.09.2018 08:51

Вы должны проанализировать разницу кода IL, созданного двумя версиями кода. Оптимизация компилятора иногда делает чудеса.

Mojtaba Tajik 09.09.2018 08:58

@FCin Добавил в сообщение структуру Vec3,

Patrik Fröhler 09.09.2018 08:58

Использование профилировщика производительности - всегда хорошая идея.

Uwe Keim 09.09.2018 08:58

@UweKeim Я компилирую в режиме выпуска, тогда профиль мне мало что говорит, в режиме отладки я не вижу такой большой разницы.

Patrik Fröhler 09.09.2018 09:07

Я создал новую форму выигрыша с этим кодом только в нем, и когда я попытался запустить ее, это было около 500 мс как с включенным, так и с отключенным возвратом, но для проекта было установлено значение .Net 3.5, поэтому я изменил его на 4.6.1, как и в другом моем project, а затем он снова пошел на 4 мс, сейчас я пытаюсь изучить материал кода IL.

Patrik Fröhler 09.09.2018 09:22

Я не могу воспроизвести переход от 400 мс до 4 мс, но после того, как я прокомментирую любой if, я получаю от 800 мс до 200-300 мс. Profiler показывает, что большая часть работы выполняется в Vec3.ProjectFast, который рассчитан для ps и pr. Посмотрев на IL, я не вижу ничего необычного. Единственная оптимизация, которую можно сделать при комментировании if, заключается в том, что ps или pr не нужно вычислять, но они занимают только ~ 10% всего времени вычислений. Вы уверены, что комментируя if, получаете 4 мс?

FCin 09.09.2018 10:13
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
11
149
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вероятно, это происходит из-за того, что при комментировании возвращаемых результатов сложность метода падает ниже порога, при котором автоматическое встраивание отключается.

Это встраивание не видно в сгенерированном 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, убедитесь, что вы вызываете его с разными или непостоянными аргументами на каждой итерации, а также убедитесь, что вы используете результат каждой итерации.

Другие вопросы по теме