Dlaczego kompilatory C ++ nie optymalizują tego warunkowego przypisania boolowskiego jako przypisania bezwarunkowego?


117

Rozważ następującą funkcję:

void func(bool& flag)
{
    if(!flag) flag=true;
}

Wydaje mi się, że jeśli flaga ma prawidłową wartość logiczną, byłoby to równoważne z bezwarunkowym ustawieniem jej na true, na przykład:

void func(bool& flag)
{
    flag=true;
}

Jednak ani gcc, ani clang nie optymalizują go w ten sposób - oba generują następujące informacje na -O3poziomie optymalizacji:

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

Moje pytanie brzmi: czy po prostu kod jest zbyt szczególny, aby go zoptymalizować, czy też istnieją dobre powody, dla których taka optymalizacja byłaby niepożądana, biorąc pod uwagę, że flagnie jest to odniesienie volatile? Wydaje się, że jedynym powodem, który może być, jest to, że flagmoże w jakiś sposób mieć wartość inną niż - truelub - falsebez niezdefiniowanego zachowania w momencie czytania, ale nie jestem pewien, czy jest to możliwe.


8
Czy masz jakieś dowody na to, że jest to „optymalizacja”?
David Schwartz,

1
@ 200_success Nie sądzę, aby umieszczenie wiersza kodu z niedziałającymi znacznikami jako tytułu było dobrą rzeczą. Jeśli chcesz mieć bardziej szczegółowy tytuł, dobrze, ale wybierz angielskie zdanie i spróbuj unikać w nim kodu (np. Dlaczego kompilatory nie optymalizują zapisów warunkowych do bezwarunkowych, skoro mogą udowodnić, że są równoważne? Lub podobne). Ponieważ znaki grawerowane nie są renderowane, nie używaj ich w tytule, nawet jeśli używasz kodu.
Bakuriu

2
@Ruslan, chociaż wydaje się, że nie wykonuje tej optymalizacji dla samej funkcji, kiedy może wbudować kod, wydaje się, że robi to dla wersji wbudowanej. Często powoduje to po prostu 1użycie stałej czasowej kompilacji . godbolt.org/g/swe0tc
Evan Teran

Odpowiedzi:


102

Może to negatywnie wpłynąć na wydajność programu ze względu na kwestie spójności pamięci podręcznej . Pisanie do flagkażdego func()wywołania spowodowałoby zanieczyszczenie zawierającej ją linii pamięci podręcznej. Stanie się tak niezależnie od tego, że zapisywana wartość dokładnie odpowiada bitom znalezionym pod adresem docelowym przed zapisem.


EDYTOWAĆ

hvd podało kolejny dobry powód, który uniemożliwia taką optymalizację. Jest to bardziej przekonujący argument przeciwko proponowanej optymalizacji, ponieważ może skutkować nieokreślonym zachowaniem, podczas gdy moja (oryginalna) odpowiedź dotyczyła tylko aspektów wydajności.

Po dłuższej refleksji mogę zaproponować jeszcze jeden przykład, dlaczego kompilatorom należy bezwzględnie zakazać - chyba że udowodnią, że transformacja jest bezpieczna w określonym kontekście - wprowadzania bezwarunkowego zapisu. Rozważ ten kod:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

Z bezwarunkowym zapisem w func()ten sposób zdecydowanie wyzwala niezdefiniowane zachowanie (zapis do pamięci tylko do odczytu zakończy program, nawet jeśli w przeciwnym razie efekt zapisu byłby bez operacji).


7
Może również pozytywnie wpłynąć na wydajność, ponieważ pozbywasz się gałęzi. Dlatego nie sądzę, aby omawianie tego konkretnego przypadku miało sens bez konkretnego systemu.
Lundin

3
Platforma docelowa nie ma wpływu na zdefiniowanie zachowania @Yakk. Mówienie, że zakończy program, jest niepoprawne, ale sam UB może mieć daleko idące konsekwencje, w tym demony nosowe.
John Dvorak

16
@Yakk To zależy od tego, co rozumie się przez „pamięć tylko do odczytu”. Nie, nie ma tego w chipie ROM, ale bardzo często znajduje się w sekcji załadowanej do strony, która nie ma włączonego dostępu do zapisu, a przy próbie zapisu do niej otrzymasz np. Sygnał SIGSEGV lub wyjątek STATUS_ACCESS_VIOLATION.
Random832

5
„To zdecydowanie wyzwala niezdefiniowane zachowanie”. Nie. Niezdefiniowane zachowanie jest własnością abstrakcyjnej maszyny. To, co mówi kod, określa, czy UB jest obecny. Kompilatory nie mogą tego spowodować (chociaż jeśli kompilator jest błędny, może spowodować niepoprawne zachowanie programów).
Eric M Schmidt

7
Jest to odrzucenie constprzekazania do funkcji, która może modyfikować dane będące źródłem niezdefiniowanego zachowania, a nie bezwarunkowy zapis. Doktorze, boli, kiedy to robię ...
Spencer,

48

Oprócz odpowiedzi Leona na temat wydajności:

Przypuśćmy, że flagtak true. Załóżmy, że dwa wątki ciągle dzwonią func(flag). Zapisana funkcja w tym przypadku nic nie przechowuje flag, więc powinna być bezpieczna dla wątków. Dwa wątki mają dostęp do tej samej pamięci, ale tylko w celu jej odczytu. Bezwarunkowe ustawienie flagna trueoznacza, że ​​dwa różne wątki będą zapisywać w tej samej pamięci. To nie jest bezpieczne, jest to niebezpieczne, nawet jeśli zapisywane dane są identyczne z danymi, które już tam są.


9
Myślę, że to efekt aplikowania [intro.races]/21.
Griwes

10
Bardzo interesujące. Więc czytam to jako: Kompilatorowi nigdy nie wolno "optymalizować w" operacji zapisu, w której abstrakcyjna maszyna by jej nie miała.
Martin Ba

3
@MartinBa Głównie tak. Ale jeśli kompilator może udowodnić, że nie ma to znaczenia, na przykład dlatego, że może udowodnić, że żaden inny wątek nie może mieć dostępu do tej konkretnej zmiennej, to może być w porządku.

13
Jest to niebezpieczne tylko wtedy, gdy system, na który jest ukierunkowany kompilator, czyni go niebezpiecznym . Nigdy nie pracowałem w systemie, w którym zapisywanie 0x01do bajtu, który już jest, 0x01powoduje „niebezpieczne” zachowanie. W systemie z dostępem do pamięci typu słowo lub dword tak by się stało; ale optymalizator powinien być tego świadomy. Na nowoczesnym komputerze PC lub telefonie nie ma problemu. Więc to nie jest ważny powód.
Yakk - Adam Nevraumont

4
@Yakk Właściwie, myśląc jeszcze więcej, myślę, że mimo wszystko jest to właściwe, nawet w przypadku zwykłych procesorów. Myślę, że masz rację, kiedy procesor może zapisywać bezpośrednio w pamięci, ale przypuśćmy, że flagjest to strona kopiowana przy zapisie. Teraz, na poziomie procesora, zachowanie może być zdefiniowane (błąd strony, niech system operacyjny to obsłuży), ale na poziomie systemu operacyjnego może być nadal niezdefiniowany, prawda?

13

Nie jestem pewien co do zachowania C ++ tutaj, ale w C pamięć może się zmienić, ponieważ jeśli pamięć zawiera niezerową wartość inną niż 1, pozostałaby niezmieniona podczas sprawdzania, ale zmieniłaby się na 1 podczas sprawdzania.

Ale ponieważ nie jestem biegły w C ++, nie wiem, czy taka sytuacja jest w ogóle możliwa.


Czy to nadal będzie prawdą _Bool?
Ruslan

5
W C, jeśli pamięć zawiera wartość, o której ABI nie mówi, że jest poprawna dla swojego typu, to jest to reprezentacja pułapki, a odczyt reprezentacji pułapki jest niezdefiniowanym zachowaniem. W C ++ może się to zdarzyć tylko podczas czytania niezainicjowanego obiektu i odczytywania niezainicjowanego obiektu, którym jest UB. Ale jeśli możesz znaleźć ABI, który mówi, że każda wartość niezerowa jest poprawna dla typu bool/ _Booli średnich true, to w tym konkretnym ABI prawdopodobnie masz rację.

1
@Ruslan W przypadku kompilatorów, które używają Itanium ABI i na procesorach ARM, C _Booli C ++ boolsą tego samego typu lub są zgodne z tymi samymi regułami. W przypadku MSVC mają ten sam rozmiar i wyrównanie, ale nie ma oficjalnego oświadczenia, czy używają tych samych reguł.
Justin Time - Przywróć Monikę

1
@JustinTime: C <stdbool.h>zawiera a typedef _Bool bool; I tak, na x86 (przynajmniej w ABI Systemu V), bool/ _Boolmuszą mieć wartość 0 lub 1, z wyczyszczonymi górnymi bitami bajtu. Nie sądzę, aby to wyjaśnienie było wiarygodne.
Peter Cordes

1
@JustinTime: To prawda, powinienem był po prostu wskazać, że zdecydowanie ma tę samą semantykę we wszystkich wersjach x86 ABI Systemu V, o co chodzi w tym pytaniu. (Mogę powiedzieć, ponieważ pierwszy argument funczostał przekazany w RDI, podczas gdy Windows używałby RDX).
Peter Cordes
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.