Я заметил, что сравнения 8-байтовых std::array создают сборку, отличную от bit_cast
ing. Кажется, что GCC делает то, что я ожидаю для массива символов, но clang генерирует дополнительную инструкцию mov (передавая аргумент по значению array<>
из 8-байтового регистра в красную зону, но все равно сравнивая аргумент регистра с указанной памятью). по другому аргументу).
В случае std::byte
мы получаем 8 отдельных однобайтовых cmp
по сравнению с одним эффективным qword для сравнения array<char>
. Интересно, есть ли причина такой разницы?
#include <array>
#include <bit>
#include <cstdint>
// produces completely different asm then the other 2 functions
bool compare1(const std::array<std::byte, 8> &p, std::array<std::byte, 8> r)
{
return p == r;
}
// seems to be similar to bit_casting, but clang generates 1 more instruction
bool compare2(const std::array<char, 8> &p, std::array<char, 8> r)
{
return p == r;
}
// same assembly if you use char instead of byte
bool compare3(const std::array<std::byte, 8> &p, std::array<std::byte, 8> r)
{
return std::bit_cast<uint64_t>(p) == std::bit_cast<uint64_t>(r);
}
ссылка на проводник компилятора
лязг асм:
compare1(std::array<std::byte, 8ul>, std::array<std::byte, 8ul>): # @compare1(std::array<std::byte, 8ul>, std::array<std::byte, 8ul>)
cmp dil, sil
sete al
jne .LBB0_8
mov eax, edi
shr eax, 8
mov ecx, esi
shr ecx, 8
cmp al, cl
sete al
jne .LBB0_8
mov eax, edi
shr eax, 16
mov ecx, esi
shr ecx, 16
cmp al, cl
sete al
jne .LBB0_8
mov eax, edi
shr eax, 24
mov ecx, esi
shr ecx, 24
cmp al, cl
sete al
jne .LBB0_8
mov rax, rdi
shr rax, 32
mov rcx, rsi
shr rcx, 32
cmp al, cl
sete al
jne .LBB0_8
mov rax, rdi
shr rax, 40
mov rcx, rsi
shr rcx, 40
cmp al, cl
sete al
jne .LBB0_8
mov rax, rdi
shr rax, 48
mov rcx, rsi
shr rcx, 48
cmp al, cl
sete al
jne .LBB0_8
xor rdi, rsi
shr rdi, 56
sete al
.LBB0_8:
ret
compare2(std::array<char, 8ul> const&, std::array<char, 8ul>): # @compare2(std::array<char, 8ul> const&, std::array<char, 8ul>)
mov qword ptr [rsp - 8], rsi
cmp qword ptr [rdi], rsi
sete al
ret
compare3(std::array<std::byte, 8ul> const&, std::array<std::byte, 8ul>): # @compare3(std::array<std::byte, 8ul> const&, std::array<std::byte, 8ul>)
cmp qword ptr [rdi], rsi
sete al
ret
GCC ассемблер:
compare1(std::array<std::byte, 8ul>, std::array<std::byte, 8ul>):
mov rdx, rdi
mov rax, rsi
cmp sil, dil
jne .L9
movzx ecx, ah
cmp dh, cl
jne .L9
mov rsi, rdi
mov rcx, rax
shr rsi, 16
shr rcx, 16
cmp sil, cl
jne .L9
mov rsi, rdi
mov rcx, rax
shr rsi, 24
shr rcx, 24
cmp sil, cl
jne .L9
mov rsi, rdi
mov rcx, rax
shr rsi, 32
shr rcx, 32
cmp sil, cl
jne .L9
mov rsi, rdi
mov rcx, rax
shr rsi, 40
shr rcx, 40
cmp sil, cl
jne .L9
mov rsi, rdi
mov rcx, rax
shr rsi, 48
shr rcx, 48
cmp sil, cl
jne .L9
shr rdx, 56
shr rax, 56
cmp dl, al
sete al
ret
.L9:
xor eax, eax
ret
compare2(std::array<char, 8ul> const&, std::array<char, 8ul>):
cmp QWORD PTR [rdi], rsi
sete al
ret
compare3(std::array<std::byte, 8ul> const&, std::array<std::byte, 8ul>):
cmp QWORD PTR [rdi], rsi
sete al
ret
MSVC оптимизирует compare1
с помощью /O2 . ИМХО, это просто неизвестная/пропущенная конкретная оптимизация для std::byte
. Вы можете сообщить об ошибке в системы отслеживания ошибок Clang и GCC.
У вас оптимизация включена? Похоже, это сохранение rsi
для временной мысли, что оно скоро привыкнет.
Это не проблема C++. Стандарт детализирует только требования к наблюдаемому поведению, а не оптимизацию компилятора. Любые причины будут зависеть от вашего компилятора/версии. Если вы создавали программу без оптимизации, этот тип оптимизации мог быть просто опущен. В противном случае это может быть просто упущенная возможность оптимизации (компиляторы обычно не «идеальны» ни по каким меркам) и может стоить, а может и не стоить отчета об ошибке. Хотя это, вероятно, и не является решающим фактором в данном случае, компиляция для обеспечения чистой производительности может означать большее количество инструкций (например, для использования конвейерной обработки или других функций конкретного процессора).
@TimRoberts да, скомпилировано с -O3. Спасибо, я думал, что это может быть пропущенная оптимизация, но не был уверен. отчет об ошибке отправлен здесь: github.com/llvm/llvm-project/issues/96990 и gcc.gnu.org/bugzilla/show_bug.cgi?id=115693
@Peter - в этом случае одна 8-байтовая cmp
намного эффективнее, чем 8 отдельных cmp
инструкций байтов по отдельности на всех процессорах x86-64 (agner.org/optimize) или любой микроархитектуре для других основных ISA. Особенно, когда для изоляции каждого байта требуется еще больше работы (например, аргументы регистра сдвига). Заставляет меня задаться вопросом, есть ли у std::array<char>
специализация для ==
или что-то в libstdc++, или есть ли что-то особенное во внутреннем типе GCC и использовании clang для std::byte
, что противоречит проходу оптимизации, который обычно объединяется в одно сравнение.
Было бы интересно посмотреть, чем и где отличается IR.
Как для Clang, так и для GCC отсутствует оптимизация.
Для GCC об этой проблеме уже сообщалось в разделе Ошибка 101485 — вызов std::equal с помощью std::byte* не использует memcmp
Разница между char
и std::byte
сводится к проблеме библиотеки.
Как выразился Джонатан Уэйкли (сопровождающий libstdc++, председатель LWG) в вашем отчете об ошибках:
у нас уже есть хак в libstdc++, он просто не работает
std::byte
std::array::operator==
реализуется с помощью std::equal
, который реализуется с использованием (см. bits/atl_algobase.h):
template<typename _II1, typename _II2>
_GLIBCXX20_CONSTEXPR
inline bool
__equal_aux1(_II1 __first1, _II1 __last1, _II2 __first2)
{
typedef typename iterator_traits<_II1>::value_type _ValueType1;
const bool __simple = ((__is_integer<_ValueType1>::__value
#if _GLIBCXX_USE_BUILTIN_TRAIT(__is_pointer)
|| __is_pointer(_ValueType1)
#endif
) && __memcmpable<_II1, _II2>::__value);
return std::__equal<__simple>::equal(__first1, __last1, __first2);
}
Когда __simple
истинно, эта функция использует std::__memcmp
для сравнения объектов, а в противном случае она делает это наивно, проходя через каждый элемент итератора.
std::byte
— это перечисление, поэтому (__is_integer<_ValueType1>::__value || __is_pointer(_ValueType1)
— это false
, и вместо него используется наивная реализация.
Оптимизация простой реализации до одного cmp
теоретически возможна, но, к сожалению, и GCC, и Clang упускают эту оптимизацию.
Определение __simple
неверно. Он рассматривает только целочисленные типы. Перечисления и тривиальные типы размером в 1 байт должны были быть включены. Любой из вышеперечисленных вариантов будет захватывать std::byte
.
@ Ян, спасибо. Как упоминалось в отчете, похоже, что это можно оптимизировать без специализации stdlib. Я бы предположил, что это прикрытие таких случаев, как #include <cstdint> struct S { char a, b, c, d; char e, f, g, h; bool operator==(const S&) const = default; }; bool compare(const S& lhs, const S& rhs) {return lhs == rhs;}
@Red.Wave Я думаю, что перечисления/тривиальные типы труднее фиксировать, потому что они могут иметь свои собственные operator==
, которые могут быть непростыми memcmp
. см. отчет об ошибках gcc
@phaile, это вполне обоснованное беспокойство. Рефлексия, возможно, единственный ответ на метапрограммирование.
Возможно, есть место для предложения std::bit_cmp
для тривиальных типов.
Какой ответ вы ищете? Для их компиляции в другой код нет семантической причины, так что это явно упущенная возможность оптимизации. Вас действительно волнуют внутренние особенности компилятора?