簡體   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