C++: Как заставить компилятор оптимизировать доступ к памяти в случае, когда указатель переменной-члена передается в другом месте

[edit: Вот мотивация: передача указателя переменной во внешнюю функцию может случайно нарушить некоторую оптимизацию для соседних переменных из-за возможности получить указатели на соседние переменные, вычисленные из исходного указателя внешней функцией. Ниже приведен исходный пост, в котором volatile предназначен для имитации внешней функции, недоступной для текущего модуля компилятора, например. вызов виртуальной функции, функция библиотеки с закрытым исходным кодом и т. д.]

Мне было интересно, будет ли return t.a; в следующем коде оптимизировано до return 0;.

//revision 1
struct T
{
    int a;
    int b;
};

void f_(int * p)
{
    *p = 1;
}
auto volatile f = f_;

int main()
{
    T t;
    t.a = 0;
    t.b = 0;
    for (int i = 0; i < 20; ++i)
    {
        f(&t.b);
    }
    return t.a;
}

Ну это не так. Достаточно справедливо, потому что код в функции f может использовать offsetof для получения указателя на t, а затем изменить t.a. Так что оптимизировать нагрузку t.a небезопасно.

[править: если подумать, offsetof здесь недостаточно. Нам нужен container_of, который, кажется, невозможно реализовать в стандартном C++.]

Но offsetof нельзя использовать для типов с нестандартной компоновкой. Итак, я попробовал следующий код:

//revision 2
#include <type_traits>

struct T
{
private:
    char dummy = 0;
public:
    int a;
    int b;
};
static_assert(!std::is_standard_layout_v<T>);

void f_(int * p)
{
    *p = 1;
}
auto volatile f = f_;

int main()
{
    T t;
    t.a = 0;
    t.b = 0;
    for (int i = 0; i < 20; ++i)
    {
        f(&t.b);
    }
    return t.a;
}

К сожалению, он все еще не работает.

Мои вопросы:

  • безопасно ли оптимизировать загрузку t.a в приведенном выше случае (ревизия 2)
  • если нет, существует ли какая-то договоренность/предложение сделать это возможным? (например, создание T более специального типа или некоторый спецификатор атрибута для члена b в T)

P.S. Следующий код оптимизирован для return t.a;, но полученный код для цикла немного неэффективен. И все же жонглирование временными переменными обременительно.

//revision 3
struct T
{
    int a;
    int b;
};

void f_(int * p)
{
    *p = 1;
}
auto volatile f = f_;

int main()
{
    T t;
    t.a = 0;
    t.b = 0;
    for (int i = 0; i < 20; ++i)
    {
        int b = t.b;
        f(&b);
        t.b = b;
    }
    return t.a;
}

person zwhconst    schedule 31.10.2020    source источник
comment
Я немного смущен. Похоже, вы изо всех сил пытаетесь не оптимизировать код, но при этом задаетесь вопросом, как заставить компилятор оптимизировать его. Возможно, было бы лучше, если бы вы вместо этого спросили, как выполнить конкретную задачу, чтобы не рисковать помещением этого вопроса в проблема XY категории.   -  person Ted Lyngmo    schedule 31.10.2020
comment
Любая конкретная причина, по которой вы используете volatile, поскольку ее правильное использование в C++ очень ограничено? Это похоже на проблему XY.   -  person Richard Critten    schedule 31.10.2020
comment
Помечая f как volatile, вы сообщаете компилятору, что f может быть изменен каким-то кодом, невидимым для компилятора. В первом случае компилятор должен учитывать возможность f() произвольного изменения в цикле и выполнения различных действий, например изменения t.a на любой итерации цикла. Было бы вполне возможно изменить f, поэтому f() делает *(p-1) = 42, поскольку это совершенно четко определенный способ изменения t.a при передаче &t.b, поскольку t.a и t.b находятся в одном и том же объекте (структуре данных). Аналогичное обсуждение ваших правок.   -  person Peter    schedule 31.10.2020
comment
@TedLyngmo, посмотри мое редактирование мотивации.   -  person zwhconst    schedule 31.10.2020
comment
@RichardCritten, посмотри мое редактирование мотивации.   -  person zwhconst    schedule 31.10.2020


Ответы (1)


Использование offsetof для достижения T::a из T::b неправомерно, так как нет объекта указатель- взаимозаменяемый с T::b, из которого можно добраться до T::a. В другом направлении это возможно достичь T::b из T::a, так как последний взаимопреобразуется указателем с T. Contra Peter в комментариях (и несмотря на существование макроса container_of например, в ядре Linux) &t.b - 1 не дает указателя на t.a, поскольку T::b и T::a не являются взаимопреобразующими указателями.

Обратите внимание, что при наличии указателя на T::a вам все равно нужно будет использовать std::launder для доступ T::b:

auto p = &t.a;
std::launder(reinterpret_cast<T*>(p))->b = 1;

Таким образом, достаточно агрессивный компилятор действительно сможет сделать вывод, что никакая замена f не может получить доступ к t.a при наличии указателя на t.b. Однако похоже, что в настоящее время ни один из основных компиляторов не выполняет эту оптимизацию.

person ecatmur    schedule 31.10.2020
comment
Я думаю, что ни один компилятор не будет активно проводить такую ​​оптимизацию из-за широко используемого макроса container_of, независимо от того, соответствует ли он требованиям или нет. Любое (даже потенциальное) __container_of подобное расширение компилятора запретит это. Поэтому нам нужно что-то вроде атрибута [[no_outer_cast]], чтобы явно разрешить это. - person zwhconst; 04.11.2020