C 全局匿名结构/联合
C global anonymous struct / union
我有一个 uint64 变量,通常只需要高或低 32 位访问。我正在使用 32 位 ARM Cortex M0,为了提高速度,我试图将 uint64 变量与 C 中的两个 uint32 变量重叠,使用匿名结构,希望避免指针算法来访问成员。
我想做的事情可行吗?可能使用命名联合也一样快,但现在我很好奇是否可以不使用命名联合。以下编译不成功:
#include <stdint.h>
volatile union {
uint64_t ab;
struct { uint32_t a, b; };
};
int main(void)
{
a = 1;
};
您定义了一个没有实例的联合,这意味着没有包含成员的联合对象。您可能会这样做:
main.h:
typedef union {
uint64_t ab;
struct { uint32_t a, b; };
} u_t;
extern u_t u;
main.c:
u_t u = { .a = 1; };
如果你真的想要(main.h):
#define a u.a
#define b u.b
#define ab u.ab
如果您使用 #define
s,请注意它们会影响 any declaration/use 标识符 (a, b, ab),即使是那些在不同的范围内。我建议您只是通过 u
对象(如 u.a
、u.b
、u.ab
)显式访问这些值。
我已从声明中删除了 volatile
,因为我非常怀疑您是否真的需要它。但是如果你愿意,你当然可以把它加回去。
(注意:问题最初将代码拆分为两个文件,main.h 和 main.c。我的答案相应地包含两个文件的代码。但是,这些可以很容易地合并为一个)。
不,您当然不能访问非实例化类型的成员。数据存储在哪里?
您应该只使 union
可见,并相信编译器会生成有效的访问。我不认为会有 "pointer arithmetic",至少在 运行-time 不会。
那样做根本不可能。全局变量(如您在头文件中声明的变量)与匿名结构或联合的成员不同,它们根本不起作用。
并且拥有匿名结构或联合对指针运算没有帮助,该结构仍将位于内存中的某个位置,并且编译器使用结构基地址的偏移量来找出成员所在的位置。然而,由于基地址和成员偏移量在编译时都是已知的,因此编译器通常能够生成代码来直接访问成员,就像任何其他变量一样。不确定的话看看生成的代码。
所以跳过匿名结构的废话,在头文件中正确定义它们,并在头文件中声明这些结构的变量,同时在一些源文件中定义这些变量。
所以对于头文件:
union you_union
{
uint64_t ab;
struct { uint32_t a, b; };
};
extern union your_union your_union;
并在源文件中
union your_union your_union;
实际上几乎没有理由使用联合。相反,可能在 inline
函数中使用 shift/mask 来提取两半:
static inline uint32_t upper(uint64_t val)
{ return (uint32_t)(val >> 32); }
static inline uint32_t lower(uint64_t val)
{ return (uint32_t)val; }
这很可能会被编译器优化为与联合方法相同的代码。
但是,正如您所指的 匿名 struct/union 成员:省略姓名是 struct/union 其中 包括 成员,而不是 struct/union 成员。所以你可以使用:
union {
uint64_t v64;
struct {
uint32_t l32;
uint32_t h32;
}; // here the name can been omitted
} my_flex_var;
问题是:
- 不可移植,可能取决于使用的ABI/PCS(尽管 AAPCS 对此很清楚,但还有其他可能有所不同)。
- 取决于小端(某些 ARM CPUs/implementations 也允许大端)。
- 不能用于已定义为
uint64_t
的实例。
- 不如函数加载那么明确。
- 可能会增加复合类型的开销(非寄存器参数传递、放置在堆栈而不是寄存器上等)
volatile 的正常用法是 load/store 它始终具有完整大小。如果不是这种情况,则可能会出现竞争条件,并且您处于 locking/mutex 等世界中,这会使事情 very 变得更加复杂。如果这两个字段只是松散相关,那么使用两个 32 位变量或其中的一个 struct
很可能会更好。
典型用法:
volatile uint64_t v64;
void int_handler(void)
{
uint64_t vt = v64;
uint32_t l = lower(vt);
uint32_t h = higher(vt);
}
这确保变量只被读取一次。使用适当的编译器,对 l
、h
的赋值不会生成任何代码,但会使用带有 vt
的寄存器。这当然取决于您的代码,但即使如果有一些开销,那也可以忽略不计。
(Sitenote:这是我作为长期嵌入式程序员的实践)
你可以花点心思使用汇编来实现你想要的,或者使用 C++。
装配中
EXPORT AB
EXPORT A
EXPORT B
AB
A SPACE 4
B SPACE 4
在 C:
extern uint64_t AB;
extern uint32_t A;
extern uint32_t B;
然后做你想做的。
在 C++ 中,是这样的:
union MyUnionType
{
uint64_t ab;
struct
{
uint32_t a;
uint32_t b;
};
} ;
MyUnionType MyUnion;
uint64_t &ab = MyUnion.ab;
uint32_t &a = MyUnion.a;
uint32_t &b = MyUnion.b;
但是,老实说,这都是白费力气。访问 a
或 MyUnion.a
将由编译器生成相同的代码。它知道所有内容的大小和偏移量,因此它不会在运行时计算任何内容,而只是从它提前知道的正确地址加载。
澄清一下我学到的东西:
原来 C 不允许匿名的全局成员 structs/unions。那好吧。但是使用 named structs/unions 无论如何都会生成高效的代码,因为成员偏移量在编译时是已知的并且可以在不产生额外指令的情况下添加。
关于使用移位和掩码而不是联合,这可能适用于读取,但对于写入它会导致额外的指令(9 对 5)和对低 uint32 的无意义访问。另一方面,它比联合更便于字节序排列,但这在我的应用程序中并不重要。
union status {
struct { uint32_t user, system; };
uint64_t all;
};
volatile union status status;
status.system |= 1u; // write to high uint32 member directly
2301 movs r3, #1
4A02 ldr r2, 0x00002910
6851 ldr r1, [r2, #4]
430B orrs r3, r1
6053 str r3, [r2, #4]
status.all |= ((uint64_t)1)<<32; // write to full uint64
2001 movs r0, #1
4905 ldr r1, 0x00002910
680C ldr r4, [r1]
684D ldr r5, [r1, #4]
4328 orrs r0, r5
1C22 adds r2, r4, #0
1C03 adds r3, r0, #0
600A str r2, [r1] // this is not atomic, and pointless
604B str r3, [r1, #4] // this is the important part
问题是关于 C 的,但可能应该提到 C++ 模式支持此语法(static
)
在 clang 和 Visual Studio 中,但在 gcc 中不存在(gcc 不支持全局未命名联合中的未命名结构):
#include <stdio.h>
#include <stdint.h>
static union {
uint32_t ab;
struct { uint16_t a, b; };
};
int main()
{
ab = 0x44332211;
printf("%x", a);
}
我有一个 uint64 变量,通常只需要高或低 32 位访问。我正在使用 32 位 ARM Cortex M0,为了提高速度,我试图将 uint64 变量与 C 中的两个 uint32 变量重叠,使用匿名结构,希望避免指针算法来访问成员。
我想做的事情可行吗?可能使用命名联合也一样快,但现在我很好奇是否可以不使用命名联合。以下编译不成功:
#include <stdint.h>
volatile union {
uint64_t ab;
struct { uint32_t a, b; };
};
int main(void)
{
a = 1;
};
您定义了一个没有实例的联合,这意味着没有包含成员的联合对象。您可能会这样做:
main.h:
typedef union {
uint64_t ab;
struct { uint32_t a, b; };
} u_t;
extern u_t u;
main.c:
u_t u = { .a = 1; };
如果你真的想要(main.h):
#define a u.a
#define b u.b
#define ab u.ab
如果您使用 #define
s,请注意它们会影响 any declaration/use 标识符 (a, b, ab),即使是那些在不同的范围内。我建议您只是通过 u
对象(如 u.a
、u.b
、u.ab
)显式访问这些值。
我已从声明中删除了 volatile
,因为我非常怀疑您是否真的需要它。但是如果你愿意,你当然可以把它加回去。
(注意:问题最初将代码拆分为两个文件,main.h 和 main.c。我的答案相应地包含两个文件的代码。但是,这些可以很容易地合并为一个)。
不,您当然不能访问非实例化类型的成员。数据存储在哪里?
您应该只使 union
可见,并相信编译器会生成有效的访问。我不认为会有 "pointer arithmetic",至少在 运行-time 不会。
那样做根本不可能。全局变量(如您在头文件中声明的变量)与匿名结构或联合的成员不同,它们根本不起作用。
并且拥有匿名结构或联合对指针运算没有帮助,该结构仍将位于内存中的某个位置,并且编译器使用结构基地址的偏移量来找出成员所在的位置。然而,由于基地址和成员偏移量在编译时都是已知的,因此编译器通常能够生成代码来直接访问成员,就像任何其他变量一样。不确定的话看看生成的代码。
所以跳过匿名结构的废话,在头文件中正确定义它们,并在头文件中声明这些结构的变量,同时在一些源文件中定义这些变量。
所以对于头文件:
union you_union
{
uint64_t ab;
struct { uint32_t a, b; };
};
extern union your_union your_union;
并在源文件中
union your_union your_union;
实际上几乎没有理由使用联合。相反,可能在 inline
函数中使用 shift/mask 来提取两半:
static inline uint32_t upper(uint64_t val)
{ return (uint32_t)(val >> 32); }
static inline uint32_t lower(uint64_t val)
{ return (uint32_t)val; }
这很可能会被编译器优化为与联合方法相同的代码。
但是,正如您所指的 匿名 struct/union 成员:省略姓名是 struct/union 其中 包括 成员,而不是 struct/union 成员。所以你可以使用:
union {
uint64_t v64;
struct {
uint32_t l32;
uint32_t h32;
}; // here the name can been omitted
} my_flex_var;
问题是:
- 不可移植,可能取决于使用的ABI/PCS(尽管 AAPCS 对此很清楚,但还有其他可能有所不同)。
- 取决于小端(某些 ARM CPUs/implementations 也允许大端)。
- 不能用于已定义为
uint64_t
的实例。 - 不如函数加载那么明确。
- 可能会增加复合类型的开销(非寄存器参数传递、放置在堆栈而不是寄存器上等)
volatile 的正常用法是 load/store 它始终具有完整大小。如果不是这种情况,则可能会出现竞争条件,并且您处于 locking/mutex 等世界中,这会使事情 very 变得更加复杂。如果这两个字段只是松散相关,那么使用两个 32 位变量或其中的一个 struct
很可能会更好。
典型用法:
volatile uint64_t v64;
void int_handler(void)
{
uint64_t vt = v64;
uint32_t l = lower(vt);
uint32_t h = higher(vt);
}
这确保变量只被读取一次。使用适当的编译器,对 l
、h
的赋值不会生成任何代码,但会使用带有 vt
的寄存器。这当然取决于您的代码,但即使如果有一些开销,那也可以忽略不计。
(Sitenote:这是我作为长期嵌入式程序员的实践)
你可以花点心思使用汇编来实现你想要的,或者使用 C++。
装配中
EXPORT AB
EXPORT A
EXPORT B
AB
A SPACE 4
B SPACE 4
在 C:
extern uint64_t AB;
extern uint32_t A;
extern uint32_t B;
然后做你想做的。
在 C++ 中,是这样的:
union MyUnionType
{
uint64_t ab;
struct
{
uint32_t a;
uint32_t b;
};
} ;
MyUnionType MyUnion;
uint64_t &ab = MyUnion.ab;
uint32_t &a = MyUnion.a;
uint32_t &b = MyUnion.b;
但是,老实说,这都是白费力气。访问 a
或 MyUnion.a
将由编译器生成相同的代码。它知道所有内容的大小和偏移量,因此它不会在运行时计算任何内容,而只是从它提前知道的正确地址加载。
澄清一下我学到的东西:
原来 C 不允许匿名的全局成员 structs/unions。那好吧。但是使用 named structs/unions 无论如何都会生成高效的代码,因为成员偏移量在编译时是已知的并且可以在不产生额外指令的情况下添加。
关于使用移位和掩码而不是联合,这可能适用于读取,但对于写入它会导致额外的指令(9 对 5)和对低 uint32 的无意义访问。另一方面,它比联合更便于字节序排列,但这在我的应用程序中并不重要。
union status {
struct { uint32_t user, system; };
uint64_t all;
};
volatile union status status;
status.system |= 1u; // write to high uint32 member directly
2301 movs r3, #1
4A02 ldr r2, 0x00002910
6851 ldr r1, [r2, #4]
430B orrs r3, r1
6053 str r3, [r2, #4]
status.all |= ((uint64_t)1)<<32; // write to full uint64
2001 movs r0, #1
4905 ldr r1, 0x00002910
680C ldr r4, [r1]
684D ldr r5, [r1, #4]
4328 orrs r0, r5
1C22 adds r2, r4, #0
1C03 adds r3, r0, #0
600A str r2, [r1] // this is not atomic, and pointless
604B str r3, [r1, #4] // this is the important part
问题是关于 C 的,但可能应该提到 C++ 模式支持此语法(static
)
在 clang 和 Visual Studio 中,但在 gcc 中不存在(gcc 不支持全局未命名联合中的未命名结构):
#include <stdio.h>
#include <stdint.h>
static union {
uint32_t ab;
struct { uint16_t a, b; };
};
int main()
{
ab = 0x44332211;
printf("%x", a);
}