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