用于创建 UEFI 应用程序的构建设置忽略了 MSVC 的代码生成期望可用的辅助函数的静态库。 MSVC 的代码生成有时会插入对辅助函数的调用,这与 gcc 在 32 位平台上执行 64x64 乘法或除法或各种其他操作的方式相同。 (例如,没有硬件 popcnt 的目标上的 popcount。)
在这种情况下,将 MSVC 手动引入不太愚蠢的代码生成(本身就是一件好事)恰好可以消除代码库中辅助函数的所有使用。这很好,但不能修复您的构建设置。如果您将来添加需要帮助程序的代码,它可能会再次崩溃. uint64_t shr(uint64_t a, unsigned c) { return a >> c; }
即使在编译时也包含对辅助函数的调用-O2
.
在没有优化的情况下按常数进行移位使用_aullshr
,而不是内联为shrd
/ shr
. 这个确切的问题(坏了-Od
构建)会重复出现uint64_t x
; x >> 4
或者你的来源中的东西。
(我不知道 MSVC 在哪里保存其辅助函数库。我们认为它是一个静态库,您可以在不引入 DLL 依赖项的情况下链接它(对于 UEFI 来说是不可能的),但我们不知道它是否可能与某些 CRT 启动捆绑在一起您需要避免链接 UEFI 的代码。)
通过这个例子,未优化与优化的问题很明显。带优化的MSVC不需要辅助函数,但它很脑残-Od
code does.
对于位域访问,MSVC 显然使用位域成员的基本类型的右移。在您的情况下,您将其设置为 64 位类型,并且 32 位 x86 没有 64 位整数移位(使用 MMX 或 SSE2 除外)。和-Od
即使对于常量计数,它也会将数据放入 EDX:EAX,将移位计数放入cl
(就像 x86 移位指令一样),并调用__aullshr
.
-
__a
= ??
-
ull
= 无符号长长。
-
shr
= 右移(如同名的 x86 asm 指令)。
- 它需要班次计数
cl
,与 x86 移位指令完全相同。
来自 Godbolt 编译器资源管理器,x86 MSVC 19.16-Od https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAM1QDsCBlZAQwBtMQBGAOgCYAOAJyCALAFYADPwDswucNIArLqVbNaoZAFJeAIR27SAZ1QBXYsg4ByAgE8ADpixUA1KdpG8wWk5es6wH4BLgCqAJIAcgAqAGwiWgDM%2BhIAglqpdo7ObrR4dC5a0skpLqUuRgTEpsgEBUXpJWWZTpiu7p7evv4aQT0ARlQA%2BgSJxWUuAPQTzdntXj7oLgPDow3j48u1VHiYrOicLiAunDGrqetlmy7bu%2Bi8h8enSWsXS0NbO3sJD09jr1c3PYiB6cMRnRoXAGfdBiB68cGvN7Da7QmIghH/d4o27SdHPc6Y5GA9D8PG6UpTRIAYUSABE6QyEvSmYzmWyXpCscTBGSOesobdOBJeQTOUToZwDkdOBixR9BfdpaNJhNqaz1SzNeyWaL%2BVyJd8lfiIXrxYLgUa/qVCrSXLpCEZZTkOgteoFuoFacwCMxwTaXGEGAwQthBgwogAlEJUqJ%2B1J4egNACymAICAwnAgoUisWBry9PpcAEoGoUrQGgyGw5Ho1FERcwkYjKZMAxKtURsbxo3m632zVuAXmAUmS4h3GTXhXBAey221UB/aCEZuMTSazjgVeKdtwGm3P%2BwRuEuV8TOPcN5wi3VyVMRzStRuABLYCPYPnjYip8y0Y5Om18rsRiYHyZYfmUX4ED%2BLgSP%2B0j0qkAGIYhvAJAmyCsKYWD3hU%2BD0NwCCJO%2BqSmAmBBxMiRgIMQECkfQFG1MwpDOvMvjINeZYuJB0HDkRREuNoSR1LSVhFqoIBWGIVikLQEkSNJqASTSegGOUZgWJgW4JJw0kEHJoliQA1iACQJNw/ASAkEgxBIggSNI/AxJwsjiVYIjSQAtlwEgSDJ%2BmkIpVjSUYIC%2BXpVjyWJcCwEgaAefYeDsGQFAQHFCVJSAABuyD2PYgyZZwgiDCcgwAB6OaQ2ysAQmDECFEB9P5fQJswxC2BJOmkHFHmYPQADytCsO1EXSVgHnqMA7D%2BfgX41HgmWYCFI2kJgpWYMgpg1R10lkbs/msHgfTEK1thUhg1idZUeBeVYOliTQ9BMGwHA8AIwjiFIsjyAoagaCgBgGKoh0hbAtDMD1IB9KgqAVKQC3EB1MTSAZAX2AQeQeBJAC0fXoMF6mWFwYmsBJUl%2BctgXlWiHlGNlLgFYI3AnC4EC4IQJBaZwzFnfFiW1Zz17Kfoei6fpRZGSZYh8BIAhiLwghCjEMQJErYiue55PyQFEnBaFpDhZFpDRVAMCICgqC80l5CUGlfMI9luX5YVxUxGVFVVTVdWUI1y3NWDbXbV1Fs9f1g3DVrY0TVNy0zet6MLUtWuretm0XTt9B7ctB1HSdZ1YIHV03XdlV0IwLBTa9QiiJIMjfT9E3/SpehA30INQGDENQzDBBw7ViPI%2BLqPo3QS047w%2BPmITV6uWTskUxJVMuDTdMM0zaKs/gRDEJz3MW%2Bl/M6AkvCCwDIv62LEtWdw71SE5/CcCI0jiGrJNuZ5IBiFL2m8JwKsSCISMRDOREJrBSOtjB6wNqJI2psICxT3nba2qUEEZQwhNaQPlKqJU9vVH2Ws/YnUDt1XqBABpDWmpgcaGho4RzwLNeOi1/LJw2ltW66caqvy1tnY6bU85p31sQa6217qlyehXPgVcPq1zrqoBu2gm6GGzm3CAHcOBd1hvDfuKNUBowxqPPq3wsYeSPhPDSRMZ7STnlrSmjksZxAEr9QI0huASBcSzNmW8d4uB5vvbeh8EgnwUaLEag8ECYGYFgBGEBiYSQ1l5EQIhuAxBkI5BIggxBHzEPwbcoDtZBQgWFC%2BpBjJXxvo5Tg99H7P1cgkSx/lArBMijEqw49ckNPPiEsS8NPB0BACIIAA%3D, with UINT64
作为位域成员类型。
;; from int Method1(unsigned __int64) PROC
...
; extract IssueStruct.Bits.field8
mov eax, DWORD PTR _IssueStruct$[ebp]
mov edx, DWORD PTR _IssueStruct$[ebp+4]
mov cl, 57 ; 00000039H
call __aullshr ; emulation of shr edx:eax, cl
and eax, 1
and edx, 0
;; then store that to memory and cmp/jcc both halves. Ultra braindead
显然,对于恒定移位和仅访问 1 位,这很容易优化,因此 MSVC 实际上并不调用辅助函数-O2
。但它的效率仍然很低!它无法完全优化掉基本类型的 64 位,即使没有一个位域宽于 32。
; x86 MSVC 19.16 -O2 with unsigned long long as the bitfield type
int Method1(unsigned __int64) PROC ; Method1, COMDAT
mov edx, DWORD PTR _Data$[esp] ; load the high half of the inputs arg
xor eax, eax ; zero the low half?!?
mov ecx, edx ; copy the high half
and ecx, 33554432 ; 02000000H ; isolate bit 57
or eax, ecx ; set flags from low |= high
je SHORT $LN2@Method1
and edx, 536870912 ; 20000000H ; isolate bit 61
xor eax, eax ; re-materialize low=0 ?!?
or eax, edx ; set flags from low |= high
je SHORT $LN2@Method1
mov eax, 1
ret 0
$LN2@Method1:
xor eax, eax
ret 0
int Method1(unsigned __int64) ENDP ; Method1
显然,这是非常愚蠢的实现0
对于低半部分而不是仅仅忽略它。如果我们将位域成员类型更改为,MSVC 会做得更好unsigned
。 (在 Godbolt 链接中,我将其更改为bf_t
所以我可以使用与 UINT64 分开的 typedef,为其他联合成员保留它。)
具有基于的结构unsigned field : 1
位域成员,MSVC 不需要帮助程序-Od
它甚至可以在以下位置编写更好的代码-O2
,所以你绝对应该在实际的生产代码中这样做。仅使用uint64_t
or unsigned long long
需要比 32 位更宽的字段成员,如果您关心 MSVC 的性能,它显然对位字段成员的 64 位类型存在错过优化的错误。
;; MSVC -O2 with plain unsigned (or uint32_t) bitfield members
int Method1(unsigned __int64) PROC ; Method1, COMDAT
mov eax, DWORD PTR _Data$[esp]
test eax, 33554432 ; 02000000H
je SHORT $LN2@Method1
test eax, 536870912 ; 20000000H
je SHORT $LN2@Method1
mov eax, 1
ret 0
$LN2@Method1:
xor eax, eax
ret 0
int Method1(unsigned __int64) ENDP ; Method1
我可能会像这样无分支地实现它((high >> 25) & (high >> 29)) & 1
with 2 shr
说明和2and
说明(以及mov
)。不过,如果确实是可预测的,那么分支就是合理的并且打破了数据依赖性。不过,clang 在这里做得很好,使用not
+ test
同时测试这两个位。 (和setcc
再次得到整数结果)。这比我的想法有更好的延迟,特别是在没有 mov-elimination 的 CPU 上。 clang 也没有错过对基于 64 位类型的位字段的优化。无论哪种方式,我们都会得到相同的代码。
# clang7.0 -O3 -m32 regardless of bitfield member type
Method1(unsigned long long): # @Method1(unsigned long long)
mov ecx, dword ptr [esp + 8]
xor eax, eax # prepare for setcc
not ecx
test ecx, 570425344 # 0x22000000
sete al
ret
UEFI编码标准:
EDK II 编码标准 5.6.3.4 位字段 https://edk2-docs.gitbooks.io/edk-ii-c-coding-standards-specification/content/5_source_files/56_declarations_and_types.html#5634-bit-fields说:
- 位字段只能是类型
INT32
, 签INT32
, UINT32
,或定义为以下三个之一的 typedef 名称INT32
变种。
我不知道为什么他们要编出这些“INT32”名称,而 C99 已经非常好了int32_t
。目前还不清楚他们为什么要实行这一限制。也许是因为 MSVC 错过优化错误?或者也许通过禁止一些“奇怪的东西”来帮助人类程序员理解。
gcc 和 clang 不会发出警告unsigned long long
作为位域类型,即使在 32 位模式下并且-Wall -Wextra -Wpedantic
,在 C 或 C++ 模式下。我不认为 ISO C 或 ISO C++ 有问题。
更远,是否应该阻止使用 int 类型的位字段? https://stackoverflow.com/questions/25940635/should-use-of-bit-fields-of-type-int-be-discouraged指出简单的int
不鼓励将其作为位域类型,因为符号是实现定义的。 ISO C++ 标准讨论了位域类型char
to long long
.
我认为你的 MSVC 关于非的警告int
位域必须来自某种编码标准执行包,因为 Godbolt 上的普通 MSVC 即使使用 `-Wall 也不会这样做。
警告 C4214:使用非标准扩展:除 int 之外的位字段类型