С++: Структурирует медленнее доступ к основным переменным?

Я нашел код, который имел "оптимизацию", как это:

void somefunc(SomeStruct param){
    float x = param.x; // param.x and x are both floats. supposedly this makes it faster access
    float y = param.y;
    float z = param.z;
}

И в комментариях сказано, что это ускорит доступ к переменной, но я всегда считал, что доступ к элементам структуры так же быстро, как если бы он был не struct.

Может ли кто-нибудь очистить мою голову от этого?

Ответ 1

Обычные правила оптимизации (Michael A. Jackson) применяются: 1. Не делайте этого. 2. (Только для экспертов:) Не делайте этого еще.

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

struct Xyz {
    float x, y, z;
};

float f(Xyz param){
    return param.x + param.y + param.z;
}

float g(Xyz param){
    float x = param.x;
    float y = param.y;
    float z = param.z;
    return x + y + z;
}

Выполнение этого процесса через LLVM показывает: Только без оптимизации оба действия действуют как ожидалось (g копирует элементы структуры в локальные сети, затем выручка суммирует их; f непосредственно выдает значения, полученные из param). Со стандартными уровнями оптимизации оба результата приводят к идентичному коду (извлекают значения один раз, а затем суммируют их).

Для короткого кода эта "оптимизация" на самом деле вредна, так как она без необходимости копирует поплавки. Для более длинного кода, использующего членов в нескольких местах, это может помочь битте за подростков, если вы активно говорите своему компилятору глупо. Быстрый тест с 65 (вместо 2) дополнений членов/локальных жителей подтверждает это: без оптимизаций f повторно загружает элементы структуры, а g повторно использует уже извлеченные локали. Оптимизированные версии снова идентичны и оба извлекают элементы только один раз. (Удивительно, но нет никакого снижения силы, увеличивая добавление в умножения даже при включенном LTO, но это просто указывает, что используемая версия LLVM не оптимизирует слишком агрессивно - так что она должна работать так же хорошо и в других компиляторах.)

Итак, нижняя строка: если вы не знаете, что ваш код должен быть скомпилирован компилятором, который так ужасно глупый и/или древний, что он ничего не оптимизирует, теперь у вас есть доказательство того, что компилятор сделает оба пути эквивалент и, таким образом, может отказаться от этого преступления против удобочитаемости и зрелости, совершенных во имя исполнения. (При необходимости повторите эксперимент для вашего конкретного компилятора.)

Ответ 2

Правило большого пальца: это не медленно, если только профайлер не говорит, что это так. Пусть компилятор беспокоится о микрооптимизации (они довольно умны в отношении них, в конце концов, они делали это в течение многих лет) и фокусируются на большей картине.

Ответ 3

Я не являюсь гуру-компилятором, поэтому возьмите это с солью. Я предполагаю, что исходный автор кода предполагает, что, копируя значения из структуры в локальные переменные, компилятор "помещает" эти переменные в регистры с плавающей запятой, которые доступны на некоторых платформах (например, x86). Если для регистрации недостаточно регистров, они будут помещены в стек.

Если говорить, что если этот код не был в середине интенсивного вычисления/цикла, я бы стремился к ясности, а не к скорости. Это довольно редко, что кто-то заметит несколько различий в настройках времени.

Ответ 4

Вам нужно будет посмотреть на скомпилированный код для конкретной реализации, но в принципе нет причин, почему ваш предпочтительный код (с использованием членов структуры) должен быть обязательно медленнее, чем код, который вы показывали ( копирование в переменные, а затем использование переменных).

someFunc принимает структуру по значению, поэтому у нее есть собственная локальная копия этой структуры. Компилятор совершенно свободен применить точно такие же оптимизации к членам структуры, как и к переменным float. Они оба являются автоматическими переменными, и в обоих случаях правило "as-if" позволяет хранить их в регистре (-ах), а не в памяти при условии, что функция создает правильное наблюдаемое поведение.

Это, если, конечно, вы не указали на структуру и не использовали ее, и в этом случае значения должны быть записаны в памяти где-то в правильном порядке, на который указывает указатель. Это начинает ограничивать оптимизацию, и другие ограничения вводятся тем фактом, что если вы передадите указатель на автоматическую переменную, компилятор больше не может считать, что имя переменной является единственной ссылкой на эту память и, следовательно, единственным способом ее содержимого могут быть изменены. Наличие нескольких ссылок на один и тот же объект называется "сглаживание" и иногда блокирует оптимизацию, которая может быть сделана, если объект каким-то образом известен как псевдоним.

И снова, если это проблема, а остальная часть кода в функции каким-то образом использует указатель на структуру, то, конечно, вы можете оказаться на изворотливой почве, копируя значения в переменные из POV правильности. Таким образом, заявленная оптимизация не так проста, как кажется в этом случае.

Теперь могут быть конкретные компиляторы (или определенные уровни оптимизации), которые не могут применяться к структурам всех оптимизаций, которые им разрешено применять, но применяют эквивалентную оптимизацию для переменных float. Если это так, то комментарий будет прав, и почему вы должны проверить, чтобы быть уверенным. Например, возможно сравнить испущенный код для этого:

float somefunc(SomeStruct param){
    float x = param.x; // param.x and x are both floats. supposedly this makes it faster access
    float y = param.y;
    float z = param.z;
    for (int i = 0; i < 10; ++i) {
        x += (y +i) * z;
    }
    return x;
}

с этим:

float somefunc(SomeStruct param){
    for (int i = 0; i < 10; ++i) {
        param.x += (param.y +i) * param.z;
    }
    return param.x;
}

Также могут быть уровни оптимизации, где дополнительные переменные делают код хуже. Я не уверен, что я очень доверяю кодовым комментариям, которые говорят, что "возможно, это делает его более быстрым", похоже, что у автора нет четкого представления о том, почему это имеет значение. "По-видимому, это ускоряет доступ - я не знаю, почему, но тесты, подтверждающие это, и чтобы продемонстрировать, что он делает заметную разницу в контексте нашей программы, находятся в исходном управлении в следующем месте", намного больше похожи на это; -)

Ответ 5

В неоптимизированном коде:

  • Параметры функции (которые не передаются по ссылке) находятся в стеке
  • локальные переменные также находятся в стеке

Неоптимизированный доступ к локальным переменным и функциональным параметрам на языке ассемблера выглядит более-менее:

mov %eax, %ebp+ compile-time-constant

где %ebp - указатель кадра (вид указателя 'this' для функции).

Не имеет значения, обращаетесь к параметру или локальной переменной.

Тот факт, что вы обращаетесь к элементу из структуры, абсолютно не отличается от точки сборки/машины. Структуры - это конструкции, сделанные на C, чтобы облегчить жизнь программиста.

Итак, явным образом, мой ответ: Нет, в этом нет никакой пользы.

Ответ 6

При использовании указателей существуют хорошие и веские причины для оптимизации такого рода, поскольку использование всех входных данных сначала освобождает компилятор от возможных проблем с псевдонимом, которые препятствуют его созданию оптимального кода (в настоящее время ограничено тоже).

Для типов без указателей теоретически существует накладные расходы, потому что каждый элемент получает доступ через struct this pointer. Теоретически это может быть заметным во внутреннем цикле и теоретически будет меньшим накладным капиталом в противном случае. На практике, однако, современный компилятор почти всегда (если только не существует сложной иерархии наследования) не производит точно такой же двоичный код.

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

Ответ 7

Компилятор может сделать более быстрый код для копирования float-to-float. Но когда x будет использоваться, он будет преобразован во внутреннее представление FPU.

Ответ 8

Когда вы укажете "простую" переменную (а не структуру/класс), которую нужно использовать, система должна только перейти в это место и получить нужные данные.

Но когда вы ссылаетесь на переменную внутри структуры или класса, например, A.B, системе необходимо вычислить, где B находится внутри этой области, называемой A (потому что могут быть другие объявленные перед ней переменные) и этот расчет занимает немного больше, чем более простой доступ, описанный выше.

Ответ 9

Настоящий ответ дается Петром. Это просто для удовольствия.

Я тестировал его. Этот код:

float somefunc(SomeStruct param, float &sum){
    float x = param.x;
    float y = param.y;
    float z = param.z;
    float xyz = x * y * z;
    sum = x + y + z;
    return xyz;
}

И этот код:

float somefunc(SomeStruct param, float &sum){
    float xyz = param.x * param.y * param.z;
    sum = param.x + param.y + param.z;
    return xyz;
}

Сгенерировать идентичный код сборки при компиляции с помощью g++ -O2. Тем не менее, они генерируют другой код с отключенной оптимизацией. Вот разница:

<   movl    -32(%rbp), %eax
<   movl    %eax, -4(%rbp)
<   movl    -28(%rbp), %eax
<   movl    %eax, -8(%rbp)
<   movl    -24(%rbp), %eax
<   movl    %eax, -12(%rbp)
<   movss   -4(%rbp), %xmm0
<   mulss   -8(%rbp), %xmm0
<   mulss   -12(%rbp), %xmm0
<   movss   %xmm0, -16(%rbp)
<   movss   -4(%rbp), %xmm0
<   addss   -8(%rbp), %xmm0
<   addss   -12(%rbp), %xmm0
---
>   movss   -32(%rbp), %xmm1
>   movss   -28(%rbp), %xmm0
>   mulss   %xmm1, %xmm0
>   movss   -24(%rbp), %xmm1
>   mulss   %xmm1, %xmm0
>   movss   %xmm0, -4(%rbp)
>   movss   -32(%rbp), %xmm1
>   movss   -28(%rbp), %xmm0
>   addss   %xmm1, %xmm0
>   movss   -24(%rbp), %xmm1
>   addss   %xmm1, %xmm0

Строки с меткой < соответствуют версии с переменными "оптимизация". Мне кажется, что "оптимизированная" версия еще медленнее, чем версия без дополнительных переменных. Однако этого следует ожидать, поскольку x, y и z выделяются в стеке точно так же, как и параметр. Какой смысл выделять больше переменных стека для дублирования существующих?

Если тот, кто сделал эту "оптимизацию", лучше знал язык, он, вероятно, объявил бы эти переменные как register, но даже это оставляет "оптимизированную" версию немного медленнее и дольше, по крайней мере, на g++/x86- 64.