Скорее всего это синдром обратного сравнения

ринд поднял суматохуУсловный переход или перемещение зависит от неинициализированных значений в одном из моих юнит-тестов.

Осмотрев сборку, я понял, что следующий код:

bool operator==(MyType const& left, MyType const& right) {
    // ... some code ...
    if (left.getA() != right.getA()) { return false; }
    // ... some code ...
    return true;
}

гдеMyType::getA() const -> std::optional<std::uint8_t>, сгенерировал следующую сборку:

   0x00000000004d9588 <+108>:   xor    eax,eax
   0x00000000004d958a <+110>:   cmp    BYTE PTR [r14+0x1d],0x0
   0x00000000004d958f <+115>:   je     0x4d9597 <... function... +123>
x  0x00000000004d9591 <+117>:   mov    r15b,BYTE PTR [r14+0x1c]
x  0x00000000004d9595 <+121>:   mov    al,0x1

   0x00000000004d9597 <+123>:   xor    edx,edx
   0x00000000004d9599 <+125>:   cmp    BYTE PTR [r13+0x1d],0x0
   0x00000000004d959e <+130>:   je     0x4d95ae <... function... +146>
x  0x00000000004d95a0 <+132>:   mov    dil,BYTE PTR [r13+0x1c]
x  0x00000000004d95a4 <+136>:   mov    dl,0x1
x  0x00000000004d95a6 <+138>:   mov    BYTE PTR [rsp+0x97],dil

   0x00000000004d95ae <+146>:   cmp    al,dl
   0x00000000004d95b0 <+148>:   jne    0x4da547 <... function... +4139>

   0x00000000004d95b6 <+154>:   cmp    r15b,BYTE PTR [rsp+0x97]
   0x00000000004d95be <+162>:   je     0x4d95c8 <... function... +172>

    => Jump on uninitialized

   0x00000000004d95c0 <+164>:   test   al,al
   0x00000000004d95c2 <+166>:   jne    0x4da547 <... function... +4139>

Где я отмеченx операторы, которые не выполняются (перепрыгиваются) в случае, если необязательный параметр НЕ установлен.

ЧленA здесь по смещению0x1c вMyType, Проверка макетаstd::optional Мы видим, что:

+0x1d соответствуетbool _M_engaged,+0x1c соответствуетstd::uint8_t _M_payload (внутри анонимного союза).

Код интереса дляstd::optional является:

constexpr explicit operator bool() const noexcept
{ return this->_M_is_engaged(); }

// Comparisons between optional values.
template<typename _Tp, typename _Up>
constexpr auto operator==(const optional<_Tp>& __lhs, const optional<_Up>& __rhs) -> __optional_relop_t<decltype(declval<_Tp>() == declval<_Up>())>
{
    return static_cast<bool>(__lhs) == static_cast<bool>(__rhs)
         && (!__lhs || *__lhs == *__rhs);
}

Здесь мы видим, что gcc довольно существенно преобразовал код; если я правильно понимаю, в C это дает:

char rsp[0x148]; // simulate the stack

/* comparisons of prior data members */

/*
0x00000000004d9588 <+108>:   xor    eax,eax
0x00000000004d958a <+110>:   cmp    BYTE PTR [r14+0x1d],0x0
0x00000000004d958f <+115>:   je     0x4d9597 <... function... +123>
0x00000000004d9591 <+117>:   mov    r15b,BYTE PTR [r14+0x1c]
0x00000000004d9595 <+121>:   mov    al,0x1
*/

int eax = 0;
if (__lhs._M_engaged == 0) { goto b123; }
bool r15b = __lhs._M_payload;
eax = 1;

b123:
/*
0x00000000004d9597 <+123>:   xor    edx,edx
0x00000000004d9599 <+125>:   cmp    BYTE PTR [r13+0x1d],0x0
0x00000000004d959e <+130>:   je     0x4d95ae <... function... +146>
0x00000000004d95a0 <+132>:   mov    dil,BYTE PTR [r13+0x1c]
0x00000000004d95a4 <+136>:   mov    dl,0x1
0x00000000004d95a6 <+138>:   mov    BYTE PTR [rsp+0x97],dil
*/

int edx = 0;
if (__rhs._M_engaged == 0) { goto b146; }
rdi = __rhs._M_payload;
edx = 1;
rsp[0x97] = rdi;

b146:
/*
0x00000000004d95ae <+146>:   cmp    al,dl
0x00000000004d95b0 <+148>:   jne    0x4da547 <... function... +4139>
*/

if (eax != edx) { goto end; } // return false

/*
0x00000000004d95b6 <+154>:   cmp    r15b,BYTE PTR [rsp+0x97]
0x00000000004d95be <+162>:   je     0x4d95c8 <... function... +172>
*/

//  Flagged by valgrind
if (r15b == rsp[097]) { goto b172; } // next data member

/*
0x00000000004d95c0 <+164>:   test   al,al
0x00000000004d95c2 <+166>:   jne    0x4da547 <... function... +4139>
*/

if (eax == 1) { goto end; } // return false

b172:

/* comparison of following data members */

end:
    return false;

Что эквивалентно:

//  Note how the operands of || are inversed.
return static_cast<bool>(__lhs) == static_cast<bool>(__rhs)
         && (*__lhs == *__rhs || !__lhs);

I думать что сборка правильная, если странно. То есть, насколько я вижу, результат сравнения неинициализированных значений фактически не влияет на результат функции (и, в отличие от C или C ++, я ожидаю, что сравнение мусора в сборке x86 НЕ будет UB):

Если один необязательныйnullopt а другой установлен, то условный переход на+148 прыгает кend (return false), ХОРОШО.Если установлены обе опции, то сравнение считывает инициализированные значения, ОК.

Таким образом, единственный случай интереса, когда оба вариантаnullopt:

если значения сравниваются равными, то код приходит к выводу, что дополнительные значения равны, что верно, так как они обаnullopt,в противном случае код заключает, что дополнительные параметры равны, если__lhs._M_engaged ложно, что правда.

В любом случае код делает вывод, что оба варианта равны, когда обаnullopt; CQFD.

Это первый случай, когда gcc генерирует явно «доброкачественные» неинициализированные чтения, и поэтому у меня есть несколько вопросов:

Неинициализировано чтение нормально в сборке (x84_64)?Является ли это синдром неудачной оптимизации||) что может сработать в не благоприятных обстоятельствах?

На данный момент я склоняюсь к аннотированию нескольких функцийoptimize(1) как обходной путь для предотвращения включения оптимизаций. К счастью, идентифицированные функции не являются критичными для производительности.

Окружающая обстановка:

компилятор: gcc 7.3флаги компиляции:-std=c++17 -g -Wall -Werror -O3 -flto (+ включает в себя)флаги ссылок:-O3 -flto (+ соответствующие библиотеки)

Примечание: может появиться с-O2 вместо-O3, но никогда без-flto.

Забавные факты

В полном коде этот шаблон появляется 32 раза в функции, описанной выше, для различных полезных нагрузок:std::uint8_t, std::uint32_t, std::uint64_t и дажеstruct { std::int64_t; std::int8_t; }.

Это появляется только в нескольких большихoperator== сравнение типов с ~ 40 членами данных, а не в меньших. И это не появляется дляstd::optional<std::string_view> даже в тех конкретных функциях (которые вызывают вstd::char_traits для сравнения).

Наконец, разъяренная изоляция рассматриваемой функции в ее собственном двоичном коде приводит к исчезновению «проблемы». Мифический MCVE оказывается неуловимым.

Ответы на вопрос(3)

Ваш ответ на вопрос