通过指针访问相同的结构或将结构存储在局部变量中?
Accessing same structs via pointers or store the struct in local variable?
我有一些遗留代码需要理解,我偶然发现在代码内部经常访问相同的结构。如果我事先保存结构的内容然后访问本地副本而不是通过指针访问,会有什么不同吗?
我已经通过在线汇编程序比较了一些测试代码,看它是否会优化代码。使用 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 是否为 "better"。当然它的可读性更好,但是保存指针的上下文是否有意义。正如我在我的文件中所说,同一个指针在同一个函数中被取消引用了大约 150 次。我知道我知道.. 这个功能绝对应该重构。 :D
没有真正的区别,因为任何优化编译器(gcc、clang)都会将其优化为堆栈变量 and/or 寄存器。
将数据复制到本地有助于让编译器证明没有其他访问通过其他指针读取或写入它。
所以基本上出于与您使用 int *restrict p
相同的原因。如果您使用 void func(struct foo *restrict ptr)
那么您就向编译器保证了对 ptr->member
的任何访问 而不是 将更改您通过任何其他指针或从global-scope变量。
Type-based 别名分析已经可以提供很大的帮助;例如,通过 float*
访问不会影响任何 int
对象。 (除非您的程序包含 strict-aliasing UB;某些编译器允许您定义该行为,例如 gcc -fno-strict-aliasing
)。
如果您不进行赋值或读取其他指针(编译器必须假设这些指针可能指向结构的成员),这不会有什么不同:别名分析将成功并让编译器将 struct 成员保存在寄存器中,以跨其他内存访问,就像它可以用于本地。
(别名分析对于当地人来说通常很容易,尤其是如果他们的地址从未被盗用,那么没有任何东西可以指向他们。)
顺便说一句,允许编译器优化非volatile
/非_Atomic
内存访问的原因是同时写入non-atomic对象是未定义的行为另一个线程读取或写入它的时间。
这使得可以安全地假设变量不会改变,除非你自己写它们,并且你不需要内存中的值在 C 抽象机中是 "in sync" 除非你做non-inline 函数调用。 (对于某些未知函数可能指向的任何对象。对于循环计数器等局部变量通常不是这种情况,因此它们可以保存在 call-preserved 寄存器中而不是 spilled/reloaded。)
但是声明局部变量持有全局变量或 pointed-to 数据的副本有一个潜在的缺点:如果编译器最终没有将局部变量保存在整个函数的寄存器中,它可能最终会必须将数据实际复制到堆栈内存中,以便可以从那里重新读取。 (如果不能证明原对象没有改变。)
通常只是在 micro-optimization 的这个水平上支持可读性,但如果您好奇的话,请查看您关心的某些平台的优化 asm。如果有很多不必要的 store/reload 发生,然后尝试使用本地人。
我有一些遗留代码需要理解,我偶然发现在代码内部经常访问相同的结构。如果我事先保存结构的内容然后访问本地副本而不是通过指针访问,会有什么不同吗?
我已经通过在线汇编程序比较了一些测试代码,看它是否会优化代码。使用 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 是否为 "better"。当然它的可读性更好,但是保存指针的上下文是否有意义。正如我在我的文件中所说,同一个指针在同一个函数中被取消引用了大约 150 次。我知道我知道.. 这个功能绝对应该重构。 :D
没有真正的区别,因为任何优化编译器(gcc、clang)都会将其优化为堆栈变量 and/or 寄存器。
将数据复制到本地有助于让编译器证明没有其他访问通过其他指针读取或写入它。
所以基本上出于与您使用 int *restrict p
相同的原因。如果您使用 void func(struct foo *restrict ptr)
那么您就向编译器保证了对 ptr->member
的任何访问 而不是 将更改您通过任何其他指针或从global-scope变量。
Type-based 别名分析已经可以提供很大的帮助;例如,通过 float*
访问不会影响任何 int
对象。 (除非您的程序包含 strict-aliasing UB;某些编译器允许您定义该行为,例如 gcc -fno-strict-aliasing
)。
如果您不进行赋值或读取其他指针(编译器必须假设这些指针可能指向结构的成员),这不会有什么不同:别名分析将成功并让编译器将 struct 成员保存在寄存器中,以跨其他内存访问,就像它可以用于本地。
(别名分析对于当地人来说通常很容易,尤其是如果他们的地址从未被盗用,那么没有任何东西可以指向他们。)
顺便说一句,允许编译器优化非volatile
/非_Atomic
内存访问的原因是同时写入non-atomic对象是未定义的行为另一个线程读取或写入它的时间。
这使得可以安全地假设变量不会改变,除非你自己写它们,并且你不需要内存中的值在 C 抽象机中是 "in sync" 除非你做non-inline 函数调用。 (对于某些未知函数可能指向的任何对象。对于循环计数器等局部变量通常不是这种情况,因此它们可以保存在 call-preserved 寄存器中而不是 spilled/reloaded。)
但是声明局部变量持有全局变量或 pointed-to 数据的副本有一个潜在的缺点:如果编译器最终没有将局部变量保存在整个函数的寄存器中,它可能最终会必须将数据实际复制到堆栈内存中,以便可以从那里重新读取。 (如果不能证明原对象没有改变。)
通常只是在 micro-optimization 的这个水平上支持可读性,但如果您好奇的话,请查看您关心的某些平台的优化 asm。如果有很多不必要的 store/reload 发生,然后尝试使用本地人。