Udostępnij za pośrednictwem


Typowe problemy przy migracji Visual C++ ARM

W przypadku korzystania z kompilatora Microsoft C++ (MSVC) ten sam kod źródłowy języka C++ może generować różne wyniki w architekturze arm niż w architekturach x86 lub x64.

Źródła problemów z migracją

Wiele problemów, które mogą wystąpić podczas migrowania kodu z architektur x86 lub x64 do architektury usługi ARM, są związane 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 uzasadnionego 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 niedeterministyczne. Chociaż zachowanie jest uznawane za niedeterministyczne, w szczególności 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 i x86 lub x64, które współdziałają ze standardem C++. Na przykład silny model pamięci architektury x86 i x64 daje volatile-kwalifikowane zmienne niektóre dodatkowe właściwości, które zostały użyte do ułatwienia niektórych rodzajów komunikacji międzywątkowa w przeszłości. Jednak słaby model pamięci architektury ARM nie obsługuje tego użycia ani nie wymaga standardu C++.

Ważne

Chociaż volatile zyskuje niektóre właściwości, które mogą służyć do implementowania ograniczonych form komunikacji międzywątkowa na x86 i x64, te dodatkowe właściwości nie są wystarczające do zaimplementowania komunikacji międzywątkowa w ogóle. Standard C++ zaleca zaimplementowanie takiej komunikacji przy użyciu odpowiednich elementów pierwotnych 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. Nawet zachowanie cytowane w tym dokumencie nie powinno być oparte na i może ulec zmianie w przyszłych kompilatorach lub implementacjach procesora 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 zmiennoprzecinkowych na niepodpisaną liczbę całkowitą

W architekturze arm konwersja wartości zmiennoprzecinkowej na 32-bitową liczbę całkowitą do najbliższej wartości, którą może reprezentować liczba całkowita, jeśli wartość zmiennoprzecinkowa znajduje się poza zakresem, który może reprezentować liczba całkowita. 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ć, ale zbyt mały, aby saturacji pełnej liczby całkowitej 32-bitowej. Konwersja również poprawnie saturates dla 32-bitowych liczb całkowitych ze znakiem, ale obcięcie nasyconych, podpisanych liczb całkowitych powoduje -1 dla dodatnio nasyconych wartości i 0 dla wartości ujemnie nasyconych. Konwersja na mniejszą liczbę całkowitą ze znakiem generuje obcięty wynik, 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. W usłudze ARM funkcja NaN konwertuje na 0x00000000; w systemach x86 i x64 są konwertowane na 0x80000000.

Konwersja zmiennoprzecinkowa może polegać tylko na tym, że wiadomo, że wartość mieści się w zakresie typu liczby całkowitej, na który 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ę w każdej wielokrotności 64 na x64, a każda wielokrotność 256 w architekturze x86, w której jest stosowana implementacja oprogramowania. 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 jest 4294967296, a wartość nie "zawija się", dopóki nie zostanie przesunięta 64 pozycje na x64 lub 256 pozycji w usłudze 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 przy użyciu procedury wewnętrznej, aby wstępnie skompilować 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 shift, co wykonywane przez procesor CPU.

Zachowanie argumentów zmiennych (varargs)

W architekturze usługi ARM parametry z listy argumentów zmiennych, które są przekazywane na stosie, podlegają wyrównaniu. 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 mocno spakować. Ta różnica może spowodować, że funkcja wariadyczna, taka jak printf odczytywanie adresów pamięci, które były przeznaczone jako dopełnienie w usłudze ARM, jeśli oczekiwany układ listy argumentów zmiennych nie jest dokładnie zgodny, mimo że może działać dla podzestawu niektórych wartości w architekturach x86 lub x64. Rozważmy następujący 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ść obliczania 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 czasami 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ążone operatory, ten kod jest tłumaczony na coś, co przypomina następujące:

Handle::acquire(operator->(memory_handle), operator*(p));

A 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ść.

zachowanie domyślne słowa kluczowego volatile

Kompilator MSVC obsługuje dwie różne interpretacje volatile kwalifikatora magazynu, które można określić za pomocą przełączników kompilatora. Przełącznik /volatile:ms wybiera semantykę microsoft extended volatile semantics, która gwarantuje silną 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ść domyślna to /volatile:iso , ponieważ procesory ARM mają słabo uporządkowany model pamięci, a ponieważ oprogramowanie ARM nie ma starszej wersji polegania na rozszerzonej semantyce /volatile:ms i zwykle nie musi interfejsu z oprogramowaniem, które to robi. Jednak czasami jest to wygodne, a nawet wymagane do skompilowania programu ARM w celu korzystania z semantyki rozszerzonej. 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 w celu odtworzenia tradycyjnych semantyki lotnych na obiektach docelowych usługi ARM kompilator musi wstawić 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 tradycyjnych semantyce lotnych i promować przenośność.

Zobacz też

Konfigurowanie Visual C++ dla procesorów ARM