Указатель арифмети c на uintptr_t - PullRequest
0 голосов
/ 15 апреля 2020

Я должен реализовать «полосовой» фильтр. Пусть a и b обозначают два целых числа, которые вызывают полуоткрытый интервал [a, b). Если какой-то аргумент x находится в этом интервале (т. Е. a <= x < b), я возвращаю указатель на C строку const char* high, в противном случае я возвращаю указатель const char* low. Ванильная реализация этой функции выглядит как

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

, которая при компиляции с -O3 -march=znver2 на Godbolt дает следующий код сборки

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

. Хотя на первый взгляд эта функция имеет больше инструкций, тщательный сравнительный анализ показывает, что она в 1,8x - 1,9 раза быстрее, чем реализация vanilla_bandpass.

Является ли это использование uintptr_t допустимым и свободным от неопределенного поведения? Я хорошо знаю, что язык вокруг uintptr_t является, по меньшей мере, неопределенным и неоднозначным, и что все, что явно не указано в стандарте (например, arithmeti c на uintptr_t), обычно считается неопределенным поведением. С другой стороны, во многих случаях стандарт явно вызывает, когда что-то имеет неопределенное поведение, чего в этом случае он также не делает. Мне известно, что «смешивание», которое происходит при объединении low_ptr и high_ptr, затрагивает темы так же, как происхождение указателя, что само по себе является мутным топи c.

Ответы [ 2 ]

0 голосов
/ 18 апреля 2020

Стандарт не требует, чтобы реализации обрабатывали uintptr_t преобразования в указатель полезным способом даже в случаях, когда значения uintptr_t получены из преобразований указатель в целое число. Например,

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

указатели px5 и py0 могут сравниваться одинаково, и независимо от того, делают они это или нет, код может использовать px5[-1] для доступа к x[4] или py0[0] для доступ y[0], но может не иметь доступа px5[0] или py0[-1]. Если указатели оказываются равными, и код пытается получить доступ к ((int*)(uintptr_t)px5)[-1], компилятор может заменить (int*)(uintptr_t)px5) на py0, так как этот указатель будет сравниваться равным px5, но затем при попытке доступа к нему перейдет по рельсам 1018 *. Аналогичным образом, если код пытается получить доступ к ((int*)(uintptr_t)py0)[0], компилятор может заменить (int*)(uintptr_t)py0 на px5, а затем перепрыгнуть через рельсы при попытке доступа к px5[0].

Хотя компилятору может показаться тупым делать такие вещи, лязг становится еще безумнее. Рассмотрим:

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

Если px и py совпадают по совпадению и i равно нулю, это заставит clang установить y[0] в 2, но вернуть 1. См. https://godbolt.org/z/7Sa_KZ для сгенерированного кода («mov eax, 1 / ret» означает «return 1»).

0 голосов
/ 15 апреля 2020

Является ли это использование uintptr_t допустимым и свободным от неопределенного поведения?

Да. Преобразование из указателя в целое число (достаточного размера, такого как uintptr_t) хорошо определено, а целочисленная арифметика c хорошо определена.

Еще один момент, на который следует обратить внимание, - это ли преобразование модифицированного uintptr_t обратно указатель возвращает то, что вы хотите. Единственная гарантия, предоставляемая стандартом, состоит в том, что указатель, преобразованный в целое число, преобразованное обратно, дает тот же адрес. К счастью, этой гарантии вам достаточно, потому что вы всегда используете точное значение из преобразованного указателя.

Если бы вы использовали что-то, кроме указателя на узкий символ, я думаю, вам нужно будет использовать std::launder на результат конверсии.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...