Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
W tym dokumencie opisano niektóre typowe problemy, które mogą wystąpić podczas migracji kodu z architektur x86 lub x64 do architektury usługi ARM. Opisano również, jak uniknąć tych problemów i jak używać kompilatora w celu ich identyfikacji.
Uwaga / Notatka
Jeśli ten artykuł odwołuje się do architektury arm, dotyczy zarówno arm32, jak i ARM64.
Źródła problemów z migracją
Wiele problemów, które możesz napotkać podczas migracji kodu z architektur x86 lub x64 do architektury ARM, związanych jest z konstrukcjami kodu źródłowego, które mogą wywoływać niezdefiniowane, zdefiniowane przez implementację lub nieokreślone zachowanie.
Niezdefiniowane zachowanie polega na zachowaniu , że standard C++ nie definiuje i jest to spowodowane przez operację, która nie ma rozsądnego wyniku: na przykład konwertowanie wartości zmiennoprzecinkowej na niepodpisaną liczbę całkowitą lub przesunięcie wartości przez liczbę pozycji ujemnych lub przekracza liczbę bitów w promowanym typie.
Zachowanie zdefiniowane przez implementację to zachowanie , które standard języka C++ wymaga od dostawcy kompilatora definiowania i dokumentowania. Program może bezpiecznie polegać na zachowaniu zdefiniowanym przez implementację, mimo że może to nie być przenośne. Przykłady zachowania zdefiniowanego przez implementację obejmują rozmiary wbudowanych typów danych i ich wymagań dotyczących wyrównania. Przykładem operacji, która może mieć wpływ na zachowanie zdefiniowane przez implementację, jest uzyskiwanie dostępu do listy argumentów zmiennych.
Nieokreślone zachowanie polega na zachowaniu , że standard C++ pozostawia celowo nieokreślone. Chociaż zachowanie jest uznawane za nieokreślone, określone wywołania nieokreślonego zachowania są określane przez implementację kompilatora. Nie ma jednak potrzeby, aby dostawca kompilatora wstępnie określał wynik lub gwarantuje spójne zachowanie między porównywalnymi wywołaniami i nie wymaga dokumentacji. Przykładem nieokreślonego zachowania jest kolejność oceniania wyrażeń podrzędnych, które obejmują argumenty wywołania funkcji.
Inne problemy z migracją mogą być przypisywane różnicom sprzętowym między architekturami ARM a x86 lub x64, które różnie współdziałają ze standardem C++. Na przykład silny model pamięci architektury x86 i x64 daje volatile
-kwalifikowanym zmiennym pewne dodatkowe właściwości, które zostały użyte do ułatwienia niektórych rodzajów komunikacji międzywątkowej w przeszłości. Jednak słaby model pamięci architektury ARM nie obsługuje tego użycia, podobnie jak nie wymaga tego standard C++.
Ważne
Chociaż volatile
zyskuje niektóre właściwości, które mogą służyć do implementowania ograniczonych form komunikacji między wątkami w x86 i x64, te właściwości nie są wystarczające do zaimplementowania komunikacji międzywątkowa w ogóle. Standard programowania C++ zaleca zaimplementowanie takiej komunikacji przy użyciu odpowiednich mechanizmów synchronizacji.
Ponieważ różne platformy mogą wyrażać różne rodzaje zachowań, przenoszenie oprogramowania między platformami może być trudne i podatne na błędy, jeśli zależy to od zachowania określonej platformy. Chociaż wiele z tych rodzajów zachowań można zaobserwować i może wydawać się stabilne, poleganie na nich jest co najmniej nieznośne, a w przypadku niezdefiniowanego lub nieokreślonego zachowania jest również błędem. pl-PL: Nawet na zachowanie cytowane w tym dokumencie nie powinno się polegać i może ulec zmianie w przyszłych kompilatorach lub implementacjach CPU.
Przykładowe problemy z migracją
W pozostałej części tego dokumentu opisano, w jaki sposób różne zachowanie tych elementów języka C++ może generować różne wyniki na różnych platformach.
Konwersja liczby zmiennoprzecinkowej na liczbę całkowitą bez znaku
Na architekturze ARM konwersja wartości zmiennoprzecinkowej na 32-bitową liczbę całkowitą nasyca do najbliższej wartości, którą może reprezentować liczba całkowita, jeśli wartość zmiennoprzecinkowa znajduje się poza zakresem, który liczba całkowita może reprezentować. W architekturach x86 i x64 konwersja zawija się, jeśli liczba całkowita jest niepodpisane, lub jest ustawiona na -2147483648, jeśli liczba całkowita jest podpisana. Żadna z tych architektur nie obsługuje bezpośrednio konwersji wartości zmiennoprzecinkowych na mniejsze typy całkowite; Zamiast tego konwersje są wykonywane do 32 bitów, a wyniki są obcinane do mniejszego rozmiaru.
W przypadku architektury ARM kombinacja nasycenia i obcinania oznacza, że konwersja na typy niepodpisane poprawnie saturuje mniejsze niepodpisane typy, gdy saturuje 32-bitową liczbę całkowitą, ale generuje obcięty wynik dla wartości, które są większe niż mniejszy typ może reprezentować, lecz są zbyt małe, aby nasycić pełen 32-bitowy integer. Konwersja również poprawnie nasyca się dla 32-bitowych całkowitych liczb ze znakiem, ale obcięcie nasyconych liczb całkowitych ze znakiem daje -1 dla dodatnio nasyconych wartości i 0 dla ujemnie nasyconych wartości. Konwersja na mniejszą liczbę całkowitą ze znakiem daje obcięty rezultat, który jest nieprzewidywalny.
W przypadku architektur x86 i x64 kombinacja zachowania zawijania dla niepodpisanych konwersji całkowitych i jawna wycena podpisanych konwersji całkowitych na przepełnienie, wraz z obcięciem, sprawia, że wyniki dla większości zmian są nieprzewidywalne, jeśli są zbyt duże.
Te platformy różnią się również sposobem obsługi konwersji wartości NaN (Not-a-Number) na typy całkowite. Na architekturze ARM wartość NaN jest konwertowana na 0x00000000; na architekturach x86 i x64 jest konwertowana na 0x80000000.
Konwersja zmiennoprzecinkowa jest bezpieczna tylko wtedy, gdy wiadomo, że wartość mieści się w zakresie docelowego typu całkowitego, do którego jest konwertowana.
Zachowanie operatora shift (<<
>>
)
W architekturze arm wartość może być przesunięta w lewo lub w prawo do 255 bitów, zanim wzorzec zacznie się powtarzać. W architekturach x86 i x64 wzorzec jest powtarzany w każdej wielokrotności 32, chyba że źródłem wzorca jest zmienna 64-bitowa. W takim przypadku wzorzec powtarza się przy każdej wielokrotności 64 na platformie x64, i przy każdej wielokrotności 256 na platformie x86, gdzie zastosowano implementację programową. Na przykład dla zmiennej 32-bitowej, która ma wartość 1 przesuniętą w lewo przez 32 pozycje, w usłudze ARM wynik wynosi 0, na x86 wynik wynosi 1, a na x64 wynik jest również 1. Jeśli jednak źródłem wartości jest zmienna 64-bitowa, wynik na wszystkich trzech platformach wynosi 4294967296, a wartość nie przechodzi w zerowy zakres, dopóki nie zostanie przesunięta o 64 pozycje na x64 lub o 256 pozycji na ARM i x86.
Ponieważ wynik operacji przesunięcia przekraczającej liczbę bitów w typie źródłowym jest niezdefiniowany, kompilator nie musi mieć spójnego zachowania we wszystkich sytuacjach. Jeśli na przykład oba operandy przesunięcia są znane w czasie kompilacji, kompilator może zoptymalizować program z pomocą wewnętrznej procedury, aby wyliczyć z góry wynik przesunięcia, a następnie podstawić wynik zamiast operacji przesunięcia. Jeśli ilość przesunięcia jest zbyt duża lub ujemna, wynik procedury wewnętrznej może być inny niż wynik tego samego wyrażenia przesunięcia, jak wykonane przez CPU.
Zachowanie argumentów zmiennych (varargs)
W architekturze ARM parametry z listy argumentów zmiennych, które są przekazywane na stosie, podlegają wyrównaniu danych. Na przykład 64-bitowy parametr jest wyrównany do granicy 64-bitowej. W przypadku architektury x86 i x64 argumenty przekazywane na stosie nie podlegają wyrównaniu i są ściśle upakowane. Ta różnica może spowodować, że funkcja wariadyczna, taka jak printf
, odczyta adresy pamięci, które były przeznaczone jako wypełnienie na ARM, jeśli oczekiwany układ listy argumentów zmiennych nie jest dokładnie dopasowany, mimo że może działać poprawnie dla podzbioru niektórych wartości w architekturach x86 lub x64. Rozważ taki przykład:
// notice that a 64-bit integer is passed to the function, but '%d' is used to read it.
// on x86 and x64 this may work for small values because %d will "parse" the low-32 bits of the argument.
// on ARM the calling convention will align the 64-bit value and the code will print a random value
printf("%d\n", 1LL);
W takim przypadku można naprawić usterkę, upewniając się, że użyto prawidłowej specyfikacji formatu, aby uwzględnić wyrównanie argumentu. Ten kod jest poprawny:
// CORRECT: use %I64d for 64-bit integers
printf("%I64d\n", 1LL);
Kolejność ewaluacji argumentów
Ponieważ procesory ARM, x86 i x64 są tak różne, mogą przedstawić różne wymagania implementacji kompilatora, a także różne możliwości optymalizacji. W związku z tym, wraz z innymi czynnikami, takimi jak wywoływanie konwencji i ustawień optymalizacji, kompilator może oceniać argumenty funkcji w innej kolejności w różnych architekturach lub w przypadku zmiany innych czynników. Może to spowodować nieoczekiwane zmianę zachowania aplikacji, która opiera się na określonej kolejności oceny.
Ten rodzaj błędu może wystąpić, gdy argumenty funkcji mają skutki uboczne wpływające na inne argumenty funkcji w tym samym wywołaniu. Zwykle tego rodzaju zależność jest łatwa do uniknięcia, ale może być zaciemniana przez zależności, które są trudne do rozpoznania lub przez przeciążenie operatora. Rozważmy ten przykład kodu:
handle memory_handle;
memory_handle->acquire(*p);
Jest to dobrze zdefiniowane, ale jeśli ->
i *
są przeciążonymi operatorami, ten kod jest tłumaczony na coś, co przypomina następujące:
Handle::acquire(operator->(memory_handle), operator*(p));
Jeśli istnieje zależność między elementami operator->(memory_handle)
i operator*(p)
, kod może polegać na określonej kolejności oceny, mimo że oryginalny kod wygląda tak, jakby nie istniała możliwa zależność.
volatile
Domyślne zachowanie słowa kluczowego
Kompilator MSVC obsługuje dwa różne znaczenia kwalifikatora pamięci volatile
, które można określić za pomocą przełączników kompilatora. Przełącznik /volatile:ms wybiera rozszerzone przez Microsoft semantyki volatile, które gwarantują szczegółową kolejność, podobnie jak w przypadku tradycyjnych systemów x86 i x64 ze względu na silny model pamięci w tych architekturach. Przełącznik /volatile:iso wybiera ścisłą semantykę lotną języka C++, która nie gwarantuje silnego porządkowania.
W architekturze ARM (z wyjątkiem ARM64EC) wartością domyślną jest /volatile:iso, ponieważ procesory ARM mają słabo uporządkowany model pamięci i oprogramowanie ARM nie ma tradycji polegania na rozszerzonej semantyce /volatile:ms oraz zwykle nie musi integrować się z oprogramowaniem, które z niej korzysta. Jednak czasami jest to wygodne, a nawet wymagane, aby skompilować program ARM, aby korzystał z rozszerzonej semantyki. Na przykład może być zbyt kosztowne przenoszenie programu do korzystania z semantyki ISO C++ lub oprogramowanie sterowników może wymagać przestrzegania tradycyjnych semantyki, aby działać poprawnie. W takich przypadkach można użyć przełącznika /volatile:ms, jednak aby odtworzyć tradycyjne semantyki volatile na docelowych procesorach ARM, kompilator musi wstawiać bariery pamięci wokół każdego odczytu lub zapisu zmiennej volatile
, aby wymusić silne porządkowanie, co może mieć negatywny wpływ na wydajność.
W architekturach x86, x64 i ARM64EC wartość domyślna to /volatile:ms , ponieważ większość oprogramowania, które zostało już utworzone dla tych architektur przy użyciu MSVC, opiera się na nich. Podczas kompilowania programów x86, x64 i ARM64EC można określić przełącznik /volatile:iso, aby uniknąć niepotrzebnego polegania na tradycyjnej semantyce zmiennych lotnych i promować przenośność.