|
1 | 1 | --- |
2 | | -title: Differences between x86 and Arm |
| 2 | +title: Overflow in floating-point to integer conversion |
3 | 3 | weight: 3 |
4 | 4 |
|
5 | 5 | ### FIXED, DO NOT MODIFY |
6 | 6 | layout: learningpathall |
7 | 7 | --- |
8 | 8 |
|
9 | | -## What are the differences in behavior between x86 and Arm floating point? |
| 9 | +## Are there differences in behavior between x86 and Arm floating point? |
10 | 10 |
|
11 | | -Although both x86 and Arm generally follow the IEEE 754 standard for floating-point representation, their behavior in edge cases — like overflow and truncation — can differ due to implementation details and instruction sets. |
| 11 | +Both the x86 and Arm architectures fully comply with the IEEE 754 standard for floating-point representation. For all well-defined operations, both architectures produce identical results. Differences only occur in cases where the IEEE 754 standard explicitly leaves behavior undefined, such as converting out-of-range floating-point values to integers. These are special undefined cases where the standard permits implementations to behave differently and is not a flaw or limitation of either architecture. |
12 | 12 |
|
13 | | -You can see this by comparing an example application on both an x86 and an Arm Linux system. |
| 13 | +Understanding these undefined corner cases will help you correct any non-portable code. |
14 | 14 |
|
15 | | -Run this example on any Linux system with x86 and Arm architecture; on AWS, use EC2 instance types `t3.micro` and `t4g.small` with Ubuntu 24.04. |
| 15 | +### Undefined behavior in floating-point to integer conversion |
16 | 16 |
|
17 | | -To learn about floating-point differences, use an editor to copy and paste the C++ code below into a new file named `converting-float.cpp`: |
| 17 | +The following example demonstrates undefined behavior that occurs when converting out-of-range floating-point values to integers. An out-of-range floating-point value is too large or too small to be represented within the limits of the floating-point format used, such as float or double. |
| 18 | + |
| 19 | +This behavior is explicitly undefined by the IEEE 754 standard and the C++ specification, meaning different architectures are permitted to handle these cases differently. |
| 20 | + |
| 21 | +The differences shown below only occur in undefined behavior cases. Normal floating-point operations produce identical results on both architectures. |
| 22 | + |
| 23 | +An example of undefined behavior in floating-point code is provided below. You can run the example application on both an x86 and an Arm Linux system. If you are using AWS, use EC2 instance types `t3.micro` and `t4g.small` with Ubuntu 24.04. |
| 24 | + |
| 25 | +To learn about floating-point conversions, use an editor to copy and paste the C++ code below into a new file named `conversions.cpp`. |
18 | 26 |
|
19 | 27 | ```cpp |
20 | 28 | #include <iostream> |
@@ -60,65 +68,65 @@ int main() { |
60 | 68 | } |
61 | 69 | ``` |
62 | 70 |
|
63 | | -If you need to install the `g++` compiler, run the commands below: |
| 71 | +If you need to install the `g++` and `clang` compilers, run the commands below: |
64 | 72 |
|
65 | 73 | ```bash |
66 | 74 | sudo apt update |
67 | | -sudo apt install g++ -y |
| 75 | +sudo apt install g++ clang -y |
68 | 76 | ``` |
69 | 77 |
|
70 | | -Compile `converting-float.cpp` on an Arm and x86 machine. |
| 78 | +Compile `conversions.cpp` on an Arm and an x86 Linux machine. |
71 | 79 |
|
72 | 80 | The compile command is the same on both systems. |
73 | 81 |
|
74 | 82 | ```bash |
75 | | -g++ converting-float.cpp -o converting-float |
| 83 | +g++ conversions.cpp -o converting-float |
| 84 | +``` |
| 85 | + |
| 86 | +Run the program on both systems: |
| 87 | + |
| 88 | +```bash |
| 89 | +./converting-float |
76 | 90 | ``` |
77 | 91 |
|
78 | 92 | For easy comparison, the image below shows the x86 output (left) and Arm output (right). The highlighted lines show the difference in output: |
79 | 93 |
|
80 | 94 |  |
81 | 95 |
|
82 | | -As you can see, there are several cases where different behavior is observed. For example when trying to convert a signed number to an unsigned number or dealing with out-of-bounds numbers. |
| 96 | +As you can see, there are several cases where different behavior is observed in these undefined scenarios. For example, when trying to convert a signed number to an unsigned number or dealing with out-of-bounds values. |
83 | 97 |
|
84 | | -## Removing hardcoded values with macros |
| 98 | +## Avoid out-of-range conversions |
85 | 99 |
|
86 | | -The above differences show that explicitly checking for specific values will lead to unportable code. |
| 100 | +The above differences demonstrate non-portable code. Undefined behavior, such as converting out-of-range floating-point values to integers, can lead to inconsistent results across platforms. To ensure portability and predictable behavior, it is essential to check for out-of-range values before performing such conversions. |
87 | 101 |
|
88 | | -For example, the function below checks if the casted result is `0`. This can be misleading — on x86, casting an out-of-range floating-point value to `uint32_t` may wrap to `0`, while on Arm it may behave differently. Relying on these results makes the code unportable. |
89 | | - |
90 | | - |
| 102 | +You can check for out-of-range values using the code below. This approach ensures that the conversion is only performed when the value is within the valid range for the target data type. If the value is out of range, a default value is used to handle the situation gracefully. This prevents unexpected results and makes the code portable. |
91 | 103 |
|
92 | 104 | ```cpp |
93 | | -void checkFloatToUint32(float num) { |
94 | | - uint32_t castedNum = static_cast<uint32_t>(num); |
95 | | - if (castedNum == 0) { |
96 | | - std::cout << "The casted number is 0, indicating that the float is out of bounds for uint32_t." << std::endl; |
| 105 | +constexpr float UINT32_MAX_F = static_cast<float>(UINT32_MAX); |
| 106 | + |
| 107 | +void convertFloatToInt(float value) { |
| 108 | + // Convert to unsigned 32-bit integer with range checking |
| 109 | + uint32_t u32; |
| 110 | + if (!std::isnan(value) && value >= 0.0f && value <= UINT32_MAX_F) { |
| 111 | + u32 = static_cast<uint32_t>(value); |
| 112 | + std::cout << "The casted number is: " << u32 << std::endl; |
97 | 113 | } else { |
98 | | - std::cout << "The casted number is: " << castedNum << std::endl; |
| 114 | + u32 = 0; // Default value for out-of-range |
| 115 | + std::cout << "The float is out of bounds for uint32_t, using 0." << std::endl; |
99 | 116 | } |
| 117 | + |
| 118 | + // ...existing code... |
100 | 119 | } |
101 | 120 | ``` |
102 | 121 |
|
103 | | -This can simply be corrected by using the macro, `UINT32_MAX`. |
| 122 | +This checking provides a portable solution that identifies out-of-range values before casting and sets the out-of-range values to 0. By incorporating such checks, you can avoid undefined behavior and ensure that your code behaves consistently across different platforms. |
104 | 123 |
|
105 | | -{{% notice Note %}} |
106 | | -To find out all the available compiler-defined macros, you can output them using: |
107 | | -```bash |
108 | | -echo "" | g++ -dM -E - |
109 | | -``` |
110 | | -{{% /notice %}} |
| 124 | +### Key takeaways |
111 | 125 |
|
112 | | -A portable version of the code is: |
| 126 | +- Arm and x86 produce identical results for all well-defined floating-point operations, both architectures comply with IEEE 754. |
| 127 | +- Differences only occur in special undefined cases where the IEEE 754 standard explicitly permits different behaviors. |
| 128 | +- An example undefined scenario is converting out-of-range floating-point values to integers. |
| 129 | +- You should avoid relying on undefined behavior to ensure portability. |
113 | 130 |
|
114 | | -```cpp |
115 | | -void checkFloatToUint32(float num) { |
116 | | - uint32_t castedNum = static_cast<uint32_t>(num); |
117 | | - if (castedNum == UINT32_MAX) { |
118 | | - std::cout << "The casted number is " << UINT32_MAX << " indicating the float was out of bounds for uint32_t." << std::endl; |
119 | | - } else { |
120 | | - std::cout << "The casted number is: " << castedNum << std::endl; |
121 | | - } |
122 | | -} |
123 | | -``` |
| 131 | +By understanding these nuances, you can confidently write code that behaves consistently across platforms. |
124 | 132 |
|
0 commit comments