Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 26.05.20 18:48
Оценка:
Что-то неожиданно понял, что всячески пытался избегать такого кода и никогда на такое не напарывался , а тут неожиданно понадобилось:
Как правильно скопировать [signed xxx] -> [unsigned xxx] и обратно ???
Сейчас ловлю UB если пытаюсь делать в лоб простым присваиванием. ВАЖНО!!! — код обязательно должен выполняться на этапе компиляции!

Рыба примерно такая:
template <typename type_t, std::enable_if_t<std::is_signed_v<type_t>, int> = 0>
constexpr std::make_unsigned_t<type_t> make_unsigned(type_t value)
{
???
}

template <typename type_t, std::enable_if_t<std::is_unsigned_v<type_t>, int> = 0>
constexpr std::make_signed_t<type_t> make_signed(type_t value)
{
???
}

P.S. Если кому интересно, нужно это так как я делаю библиотеку для работы с числами, а контейнер там содержит беззнаковые целые. Используется MSVC C++17
Re: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 26.05.20 18:57
Оценка:
...
Отредактировано 26.05.2020 19:09 Videoman . Предыдущая версия .
Re: Побитовый каст singed to unsigned
От: σ  
Дата: 26.05.20 19:25
Оценка:
bit_cast?
Re: Побитовый каст singed to unsigned
От: Alexander G Украина  
Дата: 26.05.20 19:32
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Что-то неожиданно понял, что всячески пытался избегать такого кода и никогда на такое не напарывался , а тут неожиданно понадобилось:

V>Как правильно скопировать [signed xxx] -> [unsigned xxx] и обратно ???
V>Сейчас ловлю UB если пытаюсь делать в лоб простым присваиванием. ВАЖНО!!! — код обязательно должен выполняться на этапе компиляции!

Ну если забить на перформанс, то как-то так.

template <typename type_t, std::enable_if_t<std::is_signed_v<type_t>, int> = 0>
constexpr std::make_unsigned_t<type_t> make_unsigned(type_t value)
{
    std::make_unsigned_t<type_t> high_bits = (value >> 1);
    std::make_unsigned_t<type_t> low_bits = (value & 1);
    return (high_bits << 1) | low_bits;
}

template <typename type_t, std::enable_if_t<std::is_unsigned_v<type_t>, int> = 0>
constexpr std::make_signed_t<type_t> make_signed(type_t value)
{
    std::make_signed_t<type_t> high_bits = (value >> 1);
    std::make_signed_t<type_t> low_bits = (value & 1);
    return (high_bits << 1) | low_bits;
}


(если не забивать, то надо бы что-то с этим сделать, чтобы компилятор увидел оптимизацию)
Русский военный корабль идёт ко дну!
Re: Побитовый каст singed to unsigned
От: watchmaker  
Дата: 26.05.20 19:55
Оценка: 5 (1)
Здравствуйте, Videoman, Вы писали:

V>Что-то неожиданно понял, что всячески пытался избегать такого кода и никогда на такое не напарывался , а тут неожиданно понадобилось:

V>Как правильно скопировать [signed xxx] -> [unsigned xxx] и обратно ???
Ну если прямо побитово, то std::bit_cast

V>Сейчас ловлю UB если пытаюсь делать в лоб простым присваиванием. ВАЖНО!!! — код обязательно должен выполняться на этапе компиляции!

Где ты там UB нашёл?
Преобразование signed → unsigned всегда определено.
Обратное преобразование unsigned → signed раньше было implementation-defined для непредставимых чисел, но в последней версии языка стало точно также определено через модульную арифметику.
То есть с учётом того, что тебе известна версия компилятора, для преобразования достаточно написать в теле функции return value и искать ошибки в других местах.
Re[2]: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 26.05.20 21:53
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Ну если прямо побитово, то std::bit_cast

Ну я вроде ж написал что С++17. Ну не поддерживает от memcpy в сonstexpr

W>Где ты там UB нашёл?

Я только догадываюсь, т.к. у меня в дебаге все работает, а в релизе вместо -1 оказывается 0.

W>Преобразование signed → unsigned всегда определено.

Т.е. -1 -> 0xffff....U — всегда по стандарту? Это точно?

W>Обратное преобразование unsigned → signed раньше было implementation-defined для не представимых чисел, но в последней версии языка стало точно также определено через модульную арифметику.

W>То есть с учётом того, что тебе известна версия компилятора, для преобразования достаточно написать в теле функции return value и искать ошибки в других местах.
Я использую MSVC 2017. Там С++17. Думаю что до С++ 20, для знакомых типов, много чего не стандартизировано, поэтому спрашиваю на всякий случай знающих людей.
Re[2]: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 26.05.20 21:56
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>Ну если забить на перформанс, то как-то так.


AG>
AG>template <typename type_t, std::enable_if_t<std::is_signed_v<type_t>, int> = 0>
AG>constexpr std::make_unsigned_t<type_t> make_unsigned(type_t value)
AG>{
AG>    std::make_unsigned_t<type_t> high_bits = (value >> 1);
AG>    std::make_unsigned_t<type_t> low_bits = (value & 1);
AG>    return (high_bits << 1) | low_bits;
AG>}

AG>template <typename type_t, std::enable_if_t<std::is_unsigned_v<type_t>, int> = 0>
AG>constexpr std::make_signed_t<type_t> make_signed(type_t value)
AG>{
AG>    std::make_signed_t<type_t> high_bits = (value >> 1);
AG>    std::make_signed_t<type_t> low_bits = (value & 1);
AG>    return (high_bits << 1) | low_bits;
AG>}
AG>


AG>(если не забивать, то надо бы что-то с этим сделать, чтобы компилятор увидел оптимизацию)


По-моему битовый сдвиг (>>) в рамках С++17 не определен точно для знаковых типов, разве нет?
Re[2]: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 26.05.20 21:57
Оценка:
Здравствуйте, σ, Вы писали:

σ>bit_cast?


С++17
Re[3]: Побитовый каст singed to unsigned
От: watchmaker  
Дата: 26.05.20 22:24
Оценка: 6 (1)
Здравствуйте, Videoman, Вы писали:

V>Там С++17.

Значит это implementation-defined.
Какая у тебя используется имплементация ты сам в этой же строчке написал:
V>Я использую MSVC 2017.
Открываем описание и …

When signed variables are converted to unsigned and vice versa, the bit patterns remain the same.


V>Я только догадываюсь, т.к. у меня в дебаге все работает, а в релизе вместо -1 оказывается 0.

Попробуй тогда собрать минимальный пример, что-ли, где такое получается. Или
W>> искать ошибки в других местах.
Отредактировано 26.05.2020 22:32 watchmaker . Предыдущая версия . Еще …
Отредактировано 26.05.2020 22:28 watchmaker . Предыдущая версия .
Re[4]: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 26.05.20 22:39
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Попробуй тогда собрать минимальный пример, что-ли, где такое получается.

Пробую. Это не так просто. Нужно перелопатить кучу кода. Буду пытаться. Пока, мне достаточно убедиться что signed = unsigned и наоборот, это не UВ, а implementation-defined и в большинстве распространенных компиляторов это простое побитовое копирование. Мне это важно знать что бы не смотреть всюду и сузить поиски ошибки. И все-таки UB я где-то рядом умудрился словить, в казалось бы, элементарном коде с простыми пересылками signed/unsigned из одного конструктора в другой .

W>Или искать ошибки в других местах.

И это тоже конечно буду делать.
Re[3]: Побитовый каст singed to unsigned
От: Alexander G Украина  
Дата: 27.05.20 03:54
Оценка:
Здравствуйте, Videoman, Вы писали:


V>По-моему битовый сдвиг (>>) в рамках С++17 не определен точно для знаковых типов, разве нет?


For negative a, the value of a >> b is implementation-defined (in most implementations, this performs arithmetic right shift, so that the result remains negative).

Другой варинат implementation-defined — логический сдвиг.
Но в данном случае влиять не должно, появившийся ниоткуда бит в итоге выпадает.

Т.е. для всех практически существующих реализаций будет одинаково.
Русский военный корабль идёт ко дну!
Re[2]: Побитовый каст singed to unsigned
От: σ  
Дата: 27.05.20 12:46
Оценка:
V>>Сейчас ловлю UB если пытаюсь делать в лоб простым присваиванием. ВАЖНО!!! — код обязательно должен выполняться на этапе компиляции!
W>Где ты там UB нашёл?
W>Преобразование signed → unsigned всегда определено.
W>Обратное преобразование unsigned → signed раньше было implementation-defined для непредставимых чисел, но в последней версии языка стало точно также определено через модульную арифметику.
W>То есть с учётом того, что тебе известна версия компилятора

То нужно проверить, что там преобразование unsigned → signed для непредставимых чисел не UB.
implementation-defined обязывает реализацию документировать поведение, но не обязывает делать его определённым.
Re: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 27.05.20 17:02
Оценка:
Удалось все выкинуть лишнее.
Вот такой код на С++17 в release x64 дает неожиданный для меня результат:
На godbolt я не могу найти 17-й компилятор. И там все работает.
#include <array>
#include <iostream>
#include <type_traits>

inline uint64_t addc(const uint64_t& value1, const uint64_t& value2, uint64_t& carry) noexcept
{
    assert(carry >= 0 && carry <= 1);

    uint64_t result;
    carry = _addcarry_u64(static_cast<uint8_t>(carry), value1, value2, &result);

    return result;
}

template<typename native_t = uintmax_t>
class long_t
{
public:
    static_assert(std::is_unsigned_v<native_t>, "unsigned long integer native type must be unsigned");

    using native_array_t = std::array<native_t, 2>;

    long_t() noexcept;
    constexpr long_t(const long_t& that) noexcept = default;
    constexpr long_t(long_t&& that) noexcept = default;
    constexpr long_t(native_array_t digits) noexcept;
    constexpr long_t(const native_t& value) noexcept;
    template<typename type_t, std::enable_if_t<std::is_unsigned_v<type_t>, int> = 0>
    constexpr long_t(type_t value) noexcept;
    template<typename type_t, std::enable_if_t<std::is_signed_v<type_t>, int> = 0>
    constexpr long_t(type_t value) noexcept;

    constexpr bool operator==(const long_t& that) const noexcept;
    constexpr long_t& operator+=(const long_t& that) noexcept;
    constexpr long_t operator+(const long_t& that) const noexcept;

    native_array_t digits;
};

template<typename native_t>
inline long_t<native_t>::long_t() noexcept
{
}

template<typename native_t>
inline constexpr long_t<native_t>::long_t(native_array_t digits) noexcept
: digits(digits)
{
}

template<typename native_t>
template<typename type_t, std::enable_if_t<std::is_unsigned_v<type_t>, int>>
inline constexpr long_t<native_t>::long_t(type_t value) noexcept
: long_t({ native_t(value), 0 })
{
}

template<typename native_t>
template<typename type_t, std::enable_if_t<std::is_signed_v<type_t>, int>>
inline constexpr long_t<native_t>::long_t(type_t value) noexcept
: long_t({ static_cast<native_t>(value), (value >= 0 ? 0 : native_t(~0)) })
{
}

template<typename native_t>
inline constexpr bool long_t<native_t>::operator==(const long_t& that) const noexcept
{
    if (digits[1] != that.digits[1])
        return false;

    if (digits[0] != that.digits[0])
        return false;


    return true;
}

template<typename native_t>
inline constexpr long_t<native_t>& long_t<native_t>::operator+=(const long_t& that) noexcept
{
    native_t carry = 0;

    digits[0] = addc(digits[0], that.digits[0], carry);
    digits[1] = addc(digits[1], that.digits[1], carry);

    return *this;
}

template<typename native_t>
inline constexpr long_t<native_t> long_t<native_t>::operator+(const long_t& that) const noexcept
{
    return long_t(*this) += that;
}

int main()
{
    const bool result = long_t<uintmax_t>(0) + -1 == -1;

    std::cout << "result:" << result;

    return 0;
}


На выходе:

result:0

Если реализацию метода addc поменять на С++, то все работает:
template<typename type_t, std::enable_if_t<std::is_unsigned_v<type_t>, int> = 0>
inline constexpr type_t addc(const type_t& value1, const type_t& value2, type_t& carry) noexcept
{
    const type_t tmp = value2 + carry;
    const type_t result = value1 + tmp;
    carry = (tmp < value2) || (result < value1);

    return result;
}


Ассемблерный код вот такой получается:
                  push        rbx  
00007FF751733962  sub         rsp,30h  
    const bool result = long_t<uintmax_t>(0) + -1 == -1;
00007FF751733966  xor         edx,edx  
00007FF751733968  mov         al,1  
00007FF75173396A  movzx       ecx,al  
00007FF75173396D  add         cl,0FFh  
00007FF751733970  mov         qword ptr [rsp+20h],rdx  
00007FF751733975  mov         qword ptr [rsp+28h],rdx  
00007FF75173397A  movaps      xmm0,xmmword ptr [rsp+20h]  
00007FF75173397F  psrldq      xmm0,8  
00007FF751733984  movq        rax,xmm0  
00007FF751733989  adc         rax,0FFFFFFFFFFFFFFFFh  
00007FF75173398D  cmp         rax,0FFFFFFFFFFFFFFFFh  
00007FF751733991  je          main+37h (07FF751733997h)  
00007FF751733993  xor         bl,bl  
00007FF751733995  jmp         main+39h (07FF751733999h)  
00007FF751733997  mov         bl,1  

    std::cout << "result:" << result;
00007FF751733999  mov         rcx,qword ptr [__imp_std::cout (07FF751734100h)]  
00007FF7517339A0  call        std::operator<<<std::char_traits<char> > (07FF7517313D0h)  
00007FF7517339A5  movzx       edx,bl  
00007FF7517339A8  mov         rcx,rax  
00007FF7517339AB  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7517340B8h)]  

    return 0;
00007FF7517339B1  xor         eax,eax  
}
00007FF7517339B3  add         rsp,30h  
00007FF7517339B7  pop         rbx  
00007FF7517339B8  ret
Отредактировано 27.05.2020 20:52 Videoman . Предыдущая версия . Еще …
Отредактировано 27.05.2020 17:15 Videoman . Предыдущая версия .
Отредактировано 27.05.2020 17:03 Videoman . Предыдущая версия .
Re[2]: Побитовый каст singed to unsigned
От: Alexander G Украина  
Дата: 29.05.20 06:20
Оценка:
Здравствуйте, Videoman, Вы писали:

V>

V>Ассемблерный код вот такой получается:

Ага. засада в

mov         al,1


Все вот эти манпуляции:

00007FF751733968  mov         al,1  
00007FF75173396A  movzx       ecx,al  
00007FF75173396D  add         cl,0FFh


Это занесение переменной
native_t carry = 0;

в Carry Flag (CF).

Но как видно из mov al,1 , он принимает начальное значение за 1.

По Godbolt сслыке такое же. VS2019 правильно генериует:
00007FF76A1B1015  xor         edx,edx  
...  
00007FF76A1B1029  movzx       ecx,dl  
00007FF76A1B102C  add         cl,0FFh



Как-то более прямо надо передать ему 0, как char значение, а не uintptr_t ссылку. для первой итерации в идеале вообще как compile-time константу.

Я видел этот вопрос на SO, примерно то же предложил, не видя asm




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

И если не морочить его классами, ссылками и шаблонами, то он в состоянии.
https://gcc.godbolt.org/z/arGuaM
Соотвественно, надо что-то лишнее исключить, и я бы таки со ссылок начал.
Русский военный корабль идёт ко дну!
Отредактировано 29.05.2020 6:41 Alexander G . Предыдущая версия .
Re[3]: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 29.05.20 09:18
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>
AG>00007FF751733968  mov         al,1  
AG>00007FF75173396A  movzx       ecx,al  
AG>00007FF75173396D  add         cl,0FFh 
AG>

Да, вы правы. Я сам уже 2 дня смотрю на этот ассемблерный выхлоп и офигиваю как он заносит 0 в переменную: сначала 1, а потом прибавляет к ней -1 в итоге получая таки 0 но портя операцией add флаг переноса который из-за оборота вокруг нуля становится единицей. Эта единица используется как дополнительный член суммы при следующем adc. Я правильно уловил суть?
При чем тупит он так только если смешивать суммирование положительных и отрицательных значений. Почему у меня и возникло подозрения что я где-то на UB нарвался.

AG>Как-то более прямо надо передать ему 0, как char значение, а не uintptr_t ссылку. для первой итерации в идеале вообще как compile-time константу.

Ох, у меня на самом деле огромный класс и сигнатура была выбрана именно такой для того, что бы этот код корректно работал с обобщенной шаблонной версией. А в шаблонах туда могут передаваться не только целые, но и другие классы и т.д.

AG>Вообще сама генерация adc вместо add для первого шага, с махинациями с получением значения CF -- это уже пессимизация.

AG>Компилятор должен использовать adc только для последующих шагов, где CF естественным образом получается из предыдущей инструкции.
Вообще, в более простых случаях он без проблем генерирует add вместо первого сложения. Проблема в том, что я физически не могу ему сказать делай первое сложение через add, т.к. я не нашел такого intrinsic. А иначе у меня не будет carry флага после первого сложения.

AG>И если не морочить его классами, ссылками и шаблонами, то он в состоянии.

AG>https://gcc.godbolt.org/z/arGuaM
AG>Соотвественно, надо что-то лишнее исключить, и я бы таки со ссылок начал.
Да, я сейчас пытаюсь изменить дизайн. Как получится отпишусь.
Отредактировано 29.05.2020 9:21 Videoman . Предыдущая версия . Еще …
Отредактировано 29.05.2020 9:20 Videoman . Предыдущая версия .
Re[4]: Побитовый каст singed to unsigned
От: Alexander G Украина  
Дата: 29.05.20 09:39
Оценка: 9 (1)
Здравствуйте, Videoman, Вы писали:

V>Я сам уже 2 дня смотрю на этот ассемблерный выхлоп и офигиваю как он заносит 0 в переменную: сначала 1, а потом прибавляет к ней -1 в итоге получая таки 0 но портя операцией add флаг переноса который из-за оборота вокруг нуля становится единицей. Эта единица используется как дополнительный член суммы при следующем adc. Я правильно уловил суть?


Да. И это не порча, а именно способ занесения. -1 следует понимать как беззнаковое, т.е. это 255 или 0xFF. То есть прибавить к 0 — carry flag не будет, к любому другому число — будет.

Для занесения CF = 1 есть инструкция STC, для сброса в 0 — CLC

Штука в том, что он хочет сделать это branchless — т.е. без условных переходов — т.к. бранчи, зависящие от данных могут неправильно предсказываться, и это часто приносит потери производительности.
Т.е. способ CLC / STC был бы хорош лишь для константы, при этом CLC+ADC можно выродить в ADD.

> addc(const uint64_t& value1, const uint64_t& value2, uint64_t& carry)


это очень плохая сигнатура, кроме того, что всё передайтся по ссылке, ссылка одного типа, поэтому компилятор может думать, что возможен aliasing, и генерирует безопасную дичь.
Просто замена uint64_t& carry на uint8_t& carry уже может улучшить ситуацию.
Русский военный корабль идёт ко дну!
Re[5]: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 29.05.20 10:08
Оценка:
Здравствуйте, Alexander G, Вы писали:

>> addc(const uint64_t& value1, const uint64_t& value2, uint64_t& carry)


AG>это очень плохая сигнатура, кроме того, что всё передайтся по ссылке, ссылка одного типа, поэтому компилятор может думать, что возможен aliasing, и генерирует безопасную дичь.

AG>Просто замена uint64_t& carry на uint8_t& carry уже может улучшить ситуацию.

Я знаю что дизайн не очень, но к сожалению с этим приходится мирится для очень низкоуровневых операций. Замена всех ссылок на передачу по значению приводит точно к такому же результату. На этом intrinsic сносит крышу (кстати, с вычитанием тоже самое происходит).

Вот такое изменение помогло:
inline uint64_t addc(const uint64_t& value1, const uint64_t& value2, uint8_t& carry) noexcept
{
    uint64_t result;
    carry = _addcarry_u64(carry, value1, value2, &result);

    return result;
}

Т.е. ваша догадка насчет типа carry полностью подтвердилась. Компилятор клинит где-то на стыке carry -> _addcarry_u64. Все другие изменения (замена всех ссылок на значения с возвратом кортежей или структур и т.д.) приводили к полностью идентичному ассемблерному выходу.
Спасибо вам большое за помощь! Интересно как вы догадались на счет unsigned char ???
Re[6]: Побитовый каст singed to unsigned
От: Alexander G Украина  
Дата: 29.05.20 11:00
Оценка:
V> Интересно как вы догадались на счет unsigned char ???

0. Интринсик принимает байт этим параметром
1. Подозревал так алиасинг, отличный от uint64_t тип его исключает
2. В дизасме явно видна работа с байтами al или cl, не eax/rax
3. И вообще, если забыть про архаизмы вроде #define BOOL int и VARIANT_BOOL, то булки — байты
Русский военный корабль идёт ко дну!
Re[7]: Побитовый каст singed to unsigned
От: Videoman Россия https://hts.tv/
Дата: 29.05.20 11:26
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>0. Интринсик принимает байт этим параметром

AG>1. Подозревал так алиасинг, отличный от uint64_t тип его исключает
AG>2. В дизасме явно видна работа с байтами al или cl, не eax/rax
AG>3. И вообще, если забыть про архаизмы вроде #define BOOL int и VARIANT_BOOL, то булки — байты

Да, очень вероятно что где-то внутри компилятора carry не unsigned char, а bool, и где-то типы у них скастились один в другой. Ничего, зато поправил, все тесты прошли. Теперь могу писать такой код и все работает на уровне компиляции :
constexpr auto oct = 03766713523035452062041773345651416625031020_u128;
constexpr auto dec = 338770000845734292534325025077361652240_u128;
constexpr auto hex = 0xfedcba9876543210fedcba9876543210_u128;


Еще раз, спасибо большое за помощь! В этот раз проблема действительно оказалась не простой.
Отредактировано 29.05.2020 11:26 Videoman . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.