繁体   English   中英

uintptr_t 上的指针运算

[英]Pointer arithmetic on uintptr_t

我必须实施“带通”滤波器。 ab表示产生半开区间[a, b)的两个整数。 如果某个参数x位于此区间内(即a <= x < b ),我返回一个指向 C 字符串const char* high的指针,否则我返回一个指针const char* low 这个 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);
}

当在 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

现在,我研究了创建一个没有跳转/分支的版本,看起来像这样

#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;
}

这最终只是两个指针之间的“和弦”。 使用与之前相同的选项,此代码编译为

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

虽然乍一看,这个 function 有更多的指令,但仔细的基准测试表明它比vanilla_bandpass实现快 1.8 到 1.9 倍。

uintptr_t的这种使用是否有效且没有未定义的行为? 我很清楚围绕uintptr_t的语言至少可以说是模糊和模棱两可的,并且标准中未明确指定的任何内容(如uintptr_t上的算术)通常被认为是未定义的行为。 另一方面,在许多情况下,标准会在某些事物具有未定义的行为时明确调用,而在这种情况下它也不会这样做。 我知道将low_ptrhigh_ptr加在一起时发生的“混合”触及主题,就像指针出处一样,这本身就是一个模糊的主题。

uintptr_t的这种使用是否有效且没有未定义的行为?

是的。 从指针到 integer(具有足够大小,例如uintptr_t )的转换定义明确,integer 算术定义明确。

另一件需要注意的事情是将修改后的uintptr_t回指针是否会返回您想要的内容。 标准给出的唯一保证是转换为 integer 的指针转换回给出相同的地址。 幸运的是,这种保证对你来说已经足够了,因为你总是使用来自转换指针的精确值。

如果您使用的不是指向窄字符的指针,我认为您需要对转换结果使用std::launder

该标准不要求实现以有用的方式处理uintptr_t到指针的转换,即使在uintptr_t值是从指针到整数转换产生的情况下也是如此。 给例如

extern int x[5],y[5];
int *px5 = x+5, *py0 = y;

指针px5py0可能比较相等,无论它们是否相等,代码都可以使用px5[-1]访问x[4] ,或py0[0]访问y[0] ,但不能访问px5[0]也不py0[-1] 如果指针恰好相等,并且代码尝试访问((int*)(uintptr_t)px5)[-1] ,编译器可以将(int*)(uintptr_t)px5)替换为py0 ,因为该指针比较相等到px5 ,然后在尝试访问py0[-1]时跳过轨道。 同样,如果代码尝试访问((int*)(uintptr_t)py0)[0] ,编译器可以将(int*)(uintptr_t)py0px5 ,然后在尝试访问px5[0]时跳过轨道。

虽然编译器做这样的事情看起来很迟钝,但 clang 变得更加疯狂。 考虑:

#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];
}

如果pxpy恰好相等且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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM