Доступ к тем же структурам через указатели или сохранить структуру в локальной переменной? - PullRequest
0 голосов
/ 24 июня 2019

У меня есть какой-то унаследованный код, и я наткнулся на тот факт, что внутри кода очень и очень часто осуществляется доступ к одной и той же структуре.Будет ли иметь какое-то значение, если я предварительно сохраню содержимое структуры и затем получу доступ к локальной копии вместо доступа через указатель?

Я уже сравнил некоторый тестовый код через онлайн-ассемблер, чтобы посмотреть, оптимизирует ли он код,Сделано это с https://godbolt.org/ ARM64 gcc8.2

Вариант A

typedef  struct STRUCT_D{
    int             myInt1IND;
    int             myInt2IND;
    int             myInt3IND;
    int             myInt4IND;
    int             myInt5IND;
    int             myInt6IND;
    int             myInt7IND;
    int             myInt8IND;
    int             myInt9IND;
} STRUCT_D;

typedef  struct STRUCT_C{
     STRUCT_D             myStructInDIntINC;
} STRUCT_C;

typedef  struct STRUCT_B{
    STRUCT_C *             myPointerB;
} STRUCT_B;

typedef  struct STRUCT_A{
    STRUCT_B *             myPointerA;
} STRUCT_A;


int square(void) {
    struct STRUCT_C myStructC;
    struct STRUCT_B myStructB;
    struct STRUCT_A myStructA;
    struct STRUCT_A* startPointer;
    myStructC.myStructInDIntINC.myInt1IND = 55;
    myStructB.myPointerB = &myStructC;
    myStructA.myPointerA = &myStructB;
    startPointer = &myStructA;

    int myresult = 
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt1IND + 
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt2IND +
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt3IND +
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt4IND +
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt5IND +
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt6IND +
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt7IND +
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt8IND +
    startPointer->myPointerA->myPointerB->myStructInDIntINC.myInt9IND;

    return myresult;
}

Вариант B

typedef  struct STRUCT_D{
    int             myInt1IND;
    int             myInt2IND;
    int             myInt3IND;
    int             myInt4IND;
    int             myInt5IND;
    int             myInt6IND;
    int             myInt7IND;
    int             myInt8IND;
    int             myInt9IND;
} STRUCT_D;

typedef  struct STRUCT_C{
     STRUCT_D             myStructInDIntINC;
} STRUCT_C;

typedef  struct STRUCT_B{
    STRUCT_C *             myPointerB;
} STRUCT_B;

typedef  struct STRUCT_A{
    STRUCT_B *             myPointerA;
} STRUCT_A;


int square(void) {
    struct STRUCT_C myStructC;
    struct STRUCT_B myStructB;
    struct STRUCT_A myStructA;
    struct STRUCT_A* startPointer;
    myStructC.myStructInDIntINC.myInt1IND = 55;
    myStructB.myPointerB = &myStructC;
    myStructA.myPointerA = &myStructB;
    startPointer = &myStructA;

    struct STRUCT_D myResultStruct =     startPointer->myPointerA->myPointerB->myStructInDIntINC;
    int myresult = 
    myResultStruct.myInt1IND + myResultStruct.myInt2IND +     myResultStruct.myInt3IND + 
    myResultStruct.myInt4IND + myResultStruct.myInt5IND +     myResultStruct.myInt6IND +
    myResultStruct.myInt7IND + myResultStruct.myInt8IND +     myResultStruct.myInt9IND;

    return myresult;
}

Я знаю, что STRUCT_D не полностью инициализирован,но для этого примера не актуально.Мой вопрос был бы, если вариант B "лучше".Конечно, он лучше читается, но имеет ли смысл сохранять контекст указателя.Как я уже говорил в моем файле, один и тот же указатель разыменовывается примерно в 150 раз в одной и той же функции.Я знаю, я знаю .. Эта функция должна быть рефакторирована.: D

Ответы [ 2 ]

2 голосов
/ 24 июня 2019

Не будет никакой разницы, поскольку любой оптимизирующий компилятор (gcc, clang) оптимизирует это в переменную стека и / или регистр.

0 голосов
/ 27 июня 2019

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

Так что в основном по той же причине, по которой вы использовали бы int *restrict p.Если вы используете void func(struct foo *restrict ptr), то вы обещаете компилятору, что любой доступ к ptr->member будет , а не , что приведет к изменению значения, прочитанного вами через любой другой указатель или из переменной глобальной области видимости.

Анализ псевдонимов на основе типов уже может существенно помочь;Например, доступ через float* не может повлиять на объекты int.(Если ваша программа не содержит UB со строгим псевдонимом; некоторые компиляторы позволяют вам определять это поведение, например, gcc -fno-strict-aliasing).

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

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


Кстати, причина, по которой компилятору разрешеноОптимизация доступа к памяти без volatile / не _Atomic заключается в том, что записывать неатомарный объект в то время, когда другой поток читает или записывает его, является неопределенным поведением.

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


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


Обычно предпочитают удобочитаемость по сравнению с этим уровнем микрооптимизации, но взгляните на оптимизированный asm для некоторой платформытебя волнует, если тебе любопытно.Если происходит много ненужного сохранения / перезагрузки, попробуйте использовать местные.

...