Есть ли причина, по которой сравнения 8-байтовых std::array создают разные сборки для char и std::byte?

Я заметил, что сравнения 8-байтовых std::array создают сборку, отличную от bit_casting. Кажется, что 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

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

yuri kilochek 28.06.2024 02:54

MSVC оптимизирует compare1 с помощью /O2 . ИМХО, это просто неизвестная/пропущенная конкретная оптимизация для std::byte. Вы можете сообщить об ошибке в системы отслеживания ошибок Clang и GCC.

3CxEZiVlQ 28.06.2024 02:58

У вас оптимизация включена? Похоже, это сохранение rsi для временной мысли, что оно скоро привыкнет.

Tim Roberts 28.06.2024 03:13

Это не проблема C++. Стандарт детализирует только требования к наблюдаемому поведению, а не оптимизацию компилятора. Любые причины будут зависеть от вашего компилятора/версии. Если вы создавали программу без оптимизации, этот тип оптимизации мог быть просто опущен. В противном случае это может быть просто упущенная возможность оптимизации (компиляторы обычно не «идеальны» ни по каким меркам) и может стоить, а может и не стоить отчета об ошибке. Хотя это, вероятно, и не является решающим фактором в данном случае, компиляция для обеспечения чистой производительности может означать большее количество инструкций (например, для использования конвейерной обработки или других функций конкретного процессора).

Peter 28.06.2024 03:39

@TimRoberts да, скомпилировано с -O3. Спасибо, я думал, что это может быть пропущенная оптимизация, но не был уверен. отчет об ошибке отправлен здесь: github.com/llvm/llvm-project/issues/96990 и gcc.gnu.org/bugzilla/show_bug.cgi?id=115693

phaile 28.06.2024 03:51

@Peter - в этом случае одна 8-байтовая cmp намного эффективнее, чем 8 отдельных cmp инструкций байтов по отдельности на всех процессорах x86-64 (agner.org/optimize) или любой микроархитектуре для других основных ISA. Особенно, когда для изоляции каждого байта требуется еще больше работы (например, аргументы регистра сдвига). Заставляет меня задаться вопросом, есть ли у std::array<char> специализация для == или что-то в libstdc++, или есть ли что-то особенное во внутреннем типе GCC и использовании clang для std::byte, что противоречит проходу оптимизации, который обычно объединяется в одно сравнение.

Peter Cordes 28.06.2024 04:41

Было бы интересно посмотреть, чем и где отличается IR.

Nate Eldredge 28.06.2024 07:04
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
7
176
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как для 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.

Red.Wave 28.06.2024 15:27

@ Ян, спасибо. Как упоминалось в отчете, похоже, что это можно оптимизировать без специализации 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;}

phaile 28.06.2024 15:38

@Red.Wave Я думаю, что перечисления/тривиальные типы труднее фиксировать, потому что они могут иметь свои собственные operator==, которые могут быть непростыми memcmp. см. отчет об ошибках gcc

phaile 28.06.2024 15:49

@phaile, это вполне обоснованное беспокойство. Рефлексия, возможно, единственный ответ на метапрограммирование.

Red.Wave 28.06.2024 16:01

Возможно, есть место для предложения std::bit_cmp для тривиальных типов.

Red.Wave 28.06.2024 16:08

Другие вопросы по теме

Похожие вопросы

Почему в двумерном массиве отображаются элементы, которые я не добавлял?
Android NDK: ошибка компиляции с std::execution в C++17
Можно ли определить, не имеет ли перечисление фиксированного базового типа?
Как настроить мои библиотеки и зависимости для компиляции моего приложения как статического
Принципы и практика программирования Страуструпа, 3-е издание, графика: почему передача функции обратного вызова в Window::timer_wait дает неожиданные результаты?
Почему я получаю ошибки «неопределенной ссылки» при попытке использовать OpenCV в Qt Creator с MinGW?
ESP-IDF: Модульные тесты с Unity и C++
Как передать constexpr в конструктор?
При поиске количества точек внутри некоторой фигуры с помощью KD-Tree нужно ли нам проверять пересечение областей или просто сравнивать свойства, соответствующие глубине?
Использование std::enable_if во избежание неоднозначной перегрузки шаблонных бинарных операторов