Почему простое get-утверждение так медленно?

Это было легкое задание, и мне очень нравилось работать над ним.

сегодня мне захотелось профилировать raytracer, чтобы посмотреть, смогу ли я заставить его работать быстрее (без полного пересмотра кода). Во время профилирования я заметил кое-что интересное:

    // Sphere.Intersect
    public bool Intersect(Ray ray, Intersection hit)
    {
        double a = ray.Dir.x * ray.Dir.x +
                   ray.Dir.y * ray.Dir.y +
                   ray.Dir.z * ray.Dir.z;
        double b = 2 * (ray.Dir.x * (ray.Pos.x - Center.x) +
                        ray.Dir.y * (ray.Pos.y - Center.y) +
                        ray.Dir.z * (ray.Pos.z - Center.z));
        double c = (ray.Pos.x - Center.x) * (ray.Pos.x - Center.x) +
                   (ray.Pos.y - Center.y) * (ray.Pos.y - Center.y) +
                   (ray.Pos.z - Center.z) * (ray.Pos.z - Center.z) - Radius * Radius;

        // more stuff here
    }

согласно профилировщику, 25% времени процессора было потрачено на get_Dir и get_Pos, который почему я решил оптимизировать код следующим образом:

    // Sphere.Intersect
    public bool Intersect(Ray ray, Intersection hit)
    {
        Vector3d dir = ray.Dir, pos = ray.Pos;
        double xDir = dir.x, yDir = dir.y, zDir = dir.z,
               xPos = pos.x, yPos = pos.y, zPos = pos.z,
               xCen = Center.x, yCen = Center.y, zCen = Center.z;

        double a = xDir * xDir +
                   yDir * yDir +
                   zDir * zDir;
        double b = 2 * (xDir * (xPos - xCen) +
                        yDir * (yPos - yCen) +
                        zDir * (zPos - zCen));
        double c = (xPos - xCen) * (xPos - xCen) +
                   (yPos - yCen) * (yPos - yCen) +
                   (zPos - zCen) * (zPos - zCen) - Radius * Radius;

        // more stuff here
    }

С ошеломляющие результаты.

в исходном коде запуск raytracer с его аргументами по умолчанию (создание изображения 1024x1024 только с прямой молнией и без AA) займет ~88 секунд.
В измененном коде то же самое займет чуть меньше 60 секунд.
Я достиг ускорения ~1.5 только с этой небольшой модификацией код.

сначала я думал, что геттер для Ray.Dir и Ray.Pos делали некоторые вещи за сценой,которые замедлили бы программу.

вот геттеры для обоих:

    public Vector3d Pos
    {
        get { return _pos; }
    }

    public Vector3d Dir
    {
        get { return _dir; }
    }

Итак, оба возвращают Vector3D,и все.

мне действительно интересно, как вызов геттера займет намного больше времени, чем прямой доступ к переменной.

это из-за переменных кэширования процессора? Или, может быть, накладные расходы от звонка эти методы неоднократно складывались? Или, может быть, JIT обрабатывает последний случай лучше, чем первый? Или есть что-то еще, чего я не вижу?

любые идеи были бы весьма признательны.

Edit:

как предложил @MatthewWatson, я использовал StopWatch время выпуска сборки вне отладчика. Чтобы избавиться от шума, я провел тесты несколько раз. В результате прежний код принимает ~21 секунд (между 20,7 и 20,9) закончить, тогда как последний только ~19 секунд (между 19 и 19.2).
Разница стала незначительной, но она все равно есть.

1 ответов


введение

Я готов поспорить, что исходный код намного медленнее из-за причуды В C#, включающей свойства структур типов. Это не совсем интуитивно, но этот тип собственности по своей сути медленный. Почему? Потому что структуры не передаются по ссылке. Итак, чтобы получить доступ ray.Dir.x вы должны

  1. загрузить локальную переменную ray.
  2. вызов get_Dir и сохраните результат во временной переменной. Это включает в себя копирование вся структура, даже если используется только поле "x".
  3. поле x из временной копии.

глядя на исходный код, методы доступа get вызываются 18 раз. Это огромная потеря, потому что это означает, что вся структура копируется 18 раз в целом. В вашем оптимизированном коде есть только две копии -Dir и Pos оба вызываются только один раз; дальнейший доступ к значениям состоит только из третьего шага от сверху:

  1. поле x из временной копии.

подводя итог, структуры и свойства не идут вместе.

почему C# ведет себя таким образом со свойствами структуры?

это связано с тем, что в C# структуры являются типами значений. Вы передаете само значение, а не указатель на значение.

почему компилятор не распознает, что метод доступа get просто вернувшись в поле, и в обход всего отеля?

в режиме отладки такие оптимизации пропускаются, чтобы обеспечить лучший опыт дебеггинга. Даже в режиме выпуска вы обнаружите, что большинство дрожаний не часто делают это. Я не знаю точно, почему, но я считаю, что это потому, что поле не всегда выровнено по словам. Современные процессоры имеют нечетные требования к производительности. :-)