[英]Pointer arithmetic on uintptr_t
I have to implement a "bandpass" filter.我必须实施“带通”滤波器。 Let a
and b
denote two integers that induce a half-open interval [a, b)
.令a
和b
表示产生半开区间[a, b)
的两个整数。 If some argument x
lies within this interval (ie, a <= x < b
), I return a pointer to a C string const char* high
, otherwise I return a pointer const char* low
.如果某个参数x
位于此区间内(即a <= x < b
),我返回一个指向 C 字符串const char* high
的指针,否则我返回一个指针const char* low
。 The vanilla implementation of this function looks like这个 function 的香草实现看起来像
const char* vanilla_bandpass(int a, int b, int x, const char* low,
const char* high)
{
const bool withinInterval { (a <= x) && (x < b) };
return (withinInterval ? high : low);
}
which when compiled with -O3 -march=znver2
on Godbolt gives the following assembly code当在 Godbolt 上使用-O3 -march=znver2
编译时,会给出以下汇编代码
vanilla_bandpass(int, int, int, char const*, char const*):
mov rax, r8
cmp edi, edx
jg .L4
cmp edx, esi
jge .L4
ret
.L4:
mov rax, rcx
ret
Now, I've looked into creating a version without a jump/branch, which looks like this现在,我研究了创建一个没有跳转/分支的版本,看起来像这样
#include <cstdint>
const char* funky_bandpass(int a, int b, int x, const char* low,
const char* high)
{
const bool withinInterval { (a <= x) && (x < b) };
const auto low_ptr = reinterpret_cast<uintptr_t>(low) * (!withinInterval);
const auto high_ptr = reinterpret_cast<uintptr_t>(high) * withinInterval;
const auto ptr_sum = low_ptr + high_ptr;
const auto* result = reinterpret_cast<const char*>(ptr_sum);
return result;
}
which just is ultimately just a "chord" between two pointers.这最终只是两个指针之间的“和弦”。 Using the same options as before, this code compiles to使用与之前相同的选项,此代码编译为
funky_bandpass(int, int, int, char const*, char const*):
mov r9d, esi
cmp edi, edx
mov esi, edx
setle dl
cmp esi, r9d
setl al
and edx, eax
mov eax, edx
and edx, 1
xor eax, 1
imul rdx, r8
movzx eax, al
imul rcx, rax
lea rax, [rcx+rdx]
ret
While at first glance, this function has more instructions, careful benchmarking shows that it's 1.8x to 1.9x faster than the vanilla_bandpass
implementation.虽然乍一看,这个 function 有更多的指令,但仔细的基准测试表明它比vanilla_bandpass
实现快 1.8 到 1.9 倍。
Is this use of uintptr_t
valid and free of undefined behavior? uintptr_t
的这种使用是否有效且没有未定义的行为? I'm well aware that the language around uintptr_t
is vague and ambiguous to say the least, and that anything that isn't explicitly specified in the standard (like arithmetic on uintptr_t
) is generally considered undefined behavior.我很清楚围绕uintptr_t
的语言至少可以说是模糊和模棱两可的,并且标准中未明确指定的任何内容(如uintptr_t
上的算术)通常被认为是未定义的行为。 On the other hand, in many cases, the standard explicitly calls out when something has undefined behavior, which it also doesn't do in this case.另一方面,在许多情况下,标准会在某些事物具有未定义的行为时明确调用,而在这种情况下它也不会这样做。 I'm aware that the "blending" that happens when adding together low_ptr
and high_ptr
touches on topics just as pointer provenance, which is a murky topic in and of itself.我知道将low_ptr
和high_ptr
加在一起时发生的“混合”触及主题,就像指针出处一样,这本身就是一个模糊的主题。
Is this use of
uintptr_t
valid and free of undefined behavior?uintptr_t
的这种使用是否有效且没有未定义的行为?
Yes.是的。 Conversion from pointer to integer (of sufficient size such as uintptr_t
) is well defined, and integer arithmetic is well defined.从指针到 integer(具有足够大小,例如uintptr_t
)的转换定义明确,integer 算术定义明确。
Another thing to be wary about is whether converting a modified uintptr_t
back to a pointer gives back what you want.另一件需要注意的事情是将修改后的uintptr_t
回指针是否会返回您想要的内容。 Only guarantee given by the standard is that pointer converted to integer converted back gives the same address.标准给出的唯一保证是转换为 integer 的指针转换回给出相同的地址。 Luckily, this guarantee is sufficient for you, because you always use the exact value from a converted pointer.幸运的是,这种保证对你来说已经足够了,因为你总是使用来自转换指针的精确值。
If you were using something other than pointer to narrow character, I think you would need to use std::launder
on the result of the conversion.如果您使用的不是指向窄字符的指针,我认为您需要对转换结果使用std::launder
。
The Standard doesn't require that implementations process uintptr_t
-to-pointer conversions in useful fashion even in cases where the uintptr_t
values are produced from pointer-to-integer conversions.该标准不要求实现以有用的方式处理uintptr_t
到指针的转换,即使在uintptr_t
值是从指针到整数转换产生的情况下也是如此。 Given eg给例如
extern int x[5],y[5];
int *px5 = x+5, *py0 = y;
the pointers px5
and py0
might compare equal, and regardless of whether they do or not, code may use px5[-1]
to access x[4]
, or py0[0]
to access y[0]
, but may not access px5[0]
nor py0[-1]
.指针px5
和py0
可能比较相等,无论它们是否相等,代码都可以使用px5[-1]
访问x[4]
,或py0[0]
访问y[0]
,但不能访问px5[0]
也不py0[-1]
。 If the pointers happen to be equal, and code attempts to access ((int*)(uintptr_t)px5)[-1]
, a compiler could replace (int*)(uintptr_t)px5)
with py0
, since that pointer would compare equal to px5
, but then jump the rails when attempting to access py0[-1]
.如果指针恰好相等,并且代码尝试访问((int*)(uintptr_t)px5)[-1]
,编译器可以将(int*)(uintptr_t)px5)
替换为py0
,因为该指针比较相等到px5
,然后在尝试访问py0[-1]
时跳过轨道。 Likewise if code tries to access ((int*)(uintptr_t)py0)[0]
, a compiler could replace (int*)(uintptr_t)py0
with px5
, and then jump the rails when attempting to access px5[0]
.同样,如果代码尝试访问((int*)(uintptr_t)py0)[0]
,编译器可以将(int*)(uintptr_t)py0
为px5
,然后在尝试访问px5[0]
时跳过轨道。
While it may seem obtuse for a compiler to do such a thing, clang gets even crazier.虽然编译器做这样的事情看起来很迟钝,但 clang 变得更加疯狂。 Consider:考虑:
#include <stdint.h>
extern int x[],y[];
int test(int i)
{
y[0] = 1;
uintptr_t px = (uintptr_t)(x+5);
uintptr_t py = (uintptr_t)(y+i);
int flag = (px==py);
if (flag)
y[i] = 2;
return y[0];
}
If px
and py
are coincidentally equal and i
is zero, that will cause clang to set y[0]
to 2 but return 1. See https://godbolt.org/z/7Sa_KZ for generated code ("mov eax,1 / ret" means "return 1").如果px
和py
恰好相等且i
为零,这将导致 clang 将y[0]
设置为 2 但返回 1。有关生成的代码,请参阅https://godbolt.org/z/7Sa_KZ ("mov eax,1 / ret”的意思是“返回 1”)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.