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

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

Сегодня я почувствовал, как профилировать 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 и что он.

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

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

Любые идеи были бы оценены.

Изменить:

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

Ответ 1

Введение

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

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

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

  • Поле доступа x из временной копии.

Чтобы подвести итог, структуры и свойства не совпадают.

Почему С# ведет себя так со свойствами struct?

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

Почему компилятор не признает, что get accessor просто возвращает поле и обходит свойство alltogether?

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