|
| 1 | +<- Manual.html | Back to main page |
| 2 | + |
| 3 | +Title: Vectorization |
| 4 | + |
| 5 | +One of the most important features of this framework is the ability to use vectors that are independent of processor architecture. |
| 6 | +Instead of having to manually use intrinsic functions directly, the library generates the intrinsic function calls using inlined functions bound to operands. |
| 7 | + |
| 8 | +All lanes in the vectors are indexed by memory addresses, so lane zero is always first in memory, while reinterpret casting between SIMD vectors works as if reinterpret casting pointers to bytes in memory. |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +Title2: The headers |
| 13 | + |
| 14 | +* |
| 15 | +Source/DFPSR/base/simd.h |
| 16 | + |
| 17 | +The Source/DFPSR/base/simd.h header is where the SIMD vectors are implemented. |
| 18 | + |
| 19 | +* |
| 20 | +Source/DFPSR/base/noSimd.h |
| 21 | + |
| 22 | +To simplify using the same math functions with and without vectorization, the same functionality is offered to basic scalar types in Source/DFPSR/base/noSimd.h. |
| 23 | +When writing a template function that works for both scalar and vector types, one can import Source/DFPSR/base/noSimd.h in the header that defines the template functions without polluting the header with everything inside of simd.h. |
| 24 | +Then the caller can import simd.h from within a cpp file and call the template function using the portable vector types. |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +Title2: 128-bit vector types |
| 29 | + |
| 30 | +The most deterministic and beginner friendly way of SIMD vectorizing, is to use the fixed size 128-bit vectors. |
| 31 | +Then you do not need to test the algorithm with multiple vector widths. |
| 32 | +It will just work with the same determinism as using scalar operations directly in C++. |
| 33 | + |
| 34 | +U8x16 is a vector of unsigned integer with 16 lanes of 8 bits each. |
| 35 | + |
| 36 | +U16x8 is a vector of unsigned integer with 8 lanes of 16 bits each. |
| 37 | + |
| 38 | +U32x4 is a vector of unsigned integer with 4 lanes of 32 bits each. |
| 39 | + |
| 40 | +I32x4 is a vector of signed integer with 4 lanes of 32 bits each. |
| 41 | +Note that signed integers in C++ have undefined behavior to give room for better optimization. |
| 42 | + |
| 43 | +F32x4 is a vector of floating-point numbers with 4 lanes of 32 bits each. |
| 44 | +(float, float, float, float) |
| 45 | +Even if the computer follows a standard for storage of floating-point values, the calculations are always undefined behavior when it comes to rounding errors in calculations. |
| 46 | +Use integers if you need absolute determinism regarding precision. |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +Title2: Variable width vector types |
| 51 | + |
| 52 | +When targeting hardware that has wider vectors than 128 bits, it is recommended to use the variable width SIMD vectors. |
| 53 | +Note that some SIMD hardware will have a special read-only register for reading the vector's width at runtime instead of compiling for different targets, so that the same compiled program can use the same instructions for different vector widths. |
| 54 | + |
| 55 | +For SSE2 and ARM NEON, both X and F vectors are 128 bits wide. |
| 56 | +These are usually enabled by default, because the extensions are not optional in modern hardware. |
| 57 | + |
| 58 | +For AVX, F vectors are 256 bit wide and X vectors are 128 bits wide. |
| 59 | +AVX has to be enabled using a flag specific to the compiler and will make the executable incompatible with processors that do not have the AVX extension. |
| 60 | +AVX only makes the F32xF type wider, so if you are not using it, you can skip compiling for AVX and only compile for SSE2 and AVX2. |
| 61 | +If you are not targeting old processors, you can start with AVX as the minimum requirement. |
| 62 | + |
| 63 | +For AVX2, both X and F vectors are 256 bit wide. |
| 64 | +AVX2 has to be enabled using a flag specific to the compiler and will make the executable incompatible with processors that do not have the AVX2 extension. |
| 65 | +For programs compiled with AVX2, it is advisable to also release a version of the program that does not require AVX2. |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +Title2: X vector types |
| 70 | + |
| 71 | +An X vector has the largest bit width that is available for both floating-point and integer types. |
| 72 | + |
| 73 | +* |
| 74 | +U8xX is a vector of 8-bit unsigned integers filling up the X vector width. |
| 75 | + |
| 76 | +* |
| 77 | +U16xX is a vector of 16-bit unsigned integers filling up the X vector width. |
| 78 | + |
| 79 | +* |
| 80 | +U32xX is a vector of 32-bit unsigned integers filling up the X vector width. |
| 81 | + |
| 82 | +* |
| 83 | +I32xX is a vector of 32-bit signed integers filling up the X vector width. |
| 84 | + |
| 85 | +* |
| 86 | +F32xX is a vector of 32-bit floats filling up the X vector width, which can be smaller than F32xF when X and F vectors are not of the same size. |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +Title2: F vector types |
| 91 | + |
| 92 | +An F vector has the largest bit width that is available for both floating-point only, which can then be wider than the X vector. |
| 93 | +When using the F vector, all operations in the loop must use floating-point values. |
| 94 | +If you are mixing in integers with the X vector width, you have to fall back to the X width for the entire loop. |
| 95 | + |
| 96 | +* |
| 97 | +F32xF is a vector of 32-bit floats filling up the F vector width. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +Title2: Reading and writing data |
| 102 | + |
| 103 | +The SIMD vectors are designed to be used together with dsr::Buffer and dsr::SafePointer. |
| 104 | +dsr::Buffer guarantees that the start of the buffer is aligned with both the widest SIMD vector and the widest cache line among all CPU cores. |
| 105 | +dsr::SafePointer makes it easy to catch out-of-bound errors in debug mode, even when using gather instructions. |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +Title2: readAligned |
| 110 | + |
| 111 | +readAligned is a static member function of the SIMD vectors, so you call it using the vector type as a namespace before the call. |
| 112 | +If you for example want to load an X vector of 16-bit unsigned integers, you call 'U16xX::readAligned(firstElementPointer, "Reading my vector")' to get U16xX as the result. |
| 113 | +The "data" pointer should point to the first element in the array of element to load, and must be memory aligned with the full size of the vector. |
| 114 | +The "methodName" string should be an ascii string literal, which is only used in debug mode and optimized away in release mode. |
| 115 | + |
| 116 | +U8x? U8x32::readAligned(dsr::SafePointer<const uint8_t> data, const char* methodName) |
| 117 | + |
| 118 | +U16x? U8x32::readAligned(dsr::SafePointer<const uint16_t> data, const char* methodName) |
| 119 | + |
| 120 | +U32x? U8x32::readAligned(dsr::SafePointer<const uint32_t> data, const char* methodName) |
| 121 | + |
| 122 | +I32x? U8x32::readAligned(dsr::SafePointer<const int32_t> data, const char* methodName) |
| 123 | + |
| 124 | +F32x? U8x32::readAligned(dsr::SafePointer<const float> data, const char* methodName) |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +Title2: writeAligned |
| 129 | + |
| 130 | +writeAligned is a regular member function of the SIMD vectors, so you call it by treating the vector as an object. |
| 131 | +If you have a SIMD vector called myVector containing a result that you want to store in memory, you call 'myVector.writeAligned(firstElementPointer, "Writing my vector");' to store it. |
| 132 | +The "data" pointer should point to the first element in the array of element to write, and must be memory aligned with the full size of the vector. |
| 133 | +The "methodName" string should be an ascii string literal, which is only used in debug mode and optimized away in release mode. |
| 134 | + |
| 135 | +void U8x?::writeAligned(dsr::SafePointer<uint8_t> data, const char* methodName) const |
| 136 | + |
| 137 | +void U16x?::writeAligned(dsr::SafePointer<uint16_t> data, const char* methodName) const |
| 138 | + |
| 139 | +void U32x?::writeAligned(dsr::SafePointer<uint32_t> data, const char* methodName) const |
| 140 | + |
| 141 | +void I32x?::writeAligned(dsr::SafePointer<int32_t> data, const char* methodName) const |
| 142 | + |
| 143 | +void F32x?::writeAligned(dsr::SafePointer<float> data, const char* methodName) const |
| 144 | + |
| 145 | +--- |
| 146 | + |
| 147 | +Title2: Gather functions |
| 148 | + |
| 149 | +The gather_U32, gather_I32 and gather_F32 functions take a SafePointer and a vector of 32-bit unsigned integer element offsets. |
| 150 | +For each element offset in the offset vector, the element at the pointer plus the offset is returned at the corresponding lane in the result. |
| 151 | + |
| 152 | +U32x? gather_U32(dsr::SafePointer<const uint32_t> data, const U32x? &elementOffset) |
| 153 | + |
| 154 | +I32x? gather_I32(dsr::SafePointer<const int32_t> data, const U32x? &elementOffset) |
| 155 | + |
| 156 | +F32x? gather_F32(dsr::SafePointer<const float> data, const U32x? &elementOffset) |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +Title2: Constructing SIMD vectors from multiple scalars |
| 161 | + |
| 162 | +The easiest way to construct a SIMD vector is to construct it from its elements, such as 'F32x4(0.3f, 12.5f, 10.0f, 1.0f)'. |
| 163 | + |
| 164 | +--- |
| 165 | + |
| 166 | +Title2: Constructing SIMD vectors from a single scalar |
| 167 | + |
| 168 | +Every SIMD vector can also be constructed from a uniform scalar, because this has special SIMD instructions in some processors. |
| 169 | +So 'U32x4(25)' is the same as writing 'U32x4(25, 25, 25, 25)'. |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +Title2: Constructing SIMD vectors from gradients |
| 174 | + |
| 175 | +While not hardware accellerated, the static member function createGradient can be used to construct a SIMD vector using the value of the first element and how much to add for each following element. |
| 176 | +So by writing 'F32xX::createGradient(0.5f, 1.0f)', you get F32x4(0.5f, 1.5f, 2.5f, 3.5f), F32x8(0.5f, 1.5f, 2.5f, 3.5f, 4.5f, 5.5f, 6.5f, 7.5f)... depending on the length of the type. |
| 177 | + |
| 178 | +Vectorizing sampling locations will sometimes require initializing a SIMD vector to a gradient and then iterate it in strides. |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +Title2: Math operations |
| 183 | + |
| 184 | +On top of the functions defined in noSimd.h, most of the standard math functions that applies to scalars are also defined for SIMD vectors, such as addition (a + b), subtraction (a - b, -a) and multiplication (a * b). |
| 185 | +The math operations are performed as point operations. |
| 186 | +(a, b, c, d...) op (x, y, z, w...) = (a op x, b op y, c op z, d op w...) |
| 187 | + |
| 188 | +32-bit integer multiplication is not supported directly in hardware on some processors, but it will fall back on multiple scalar multiplications when that happens to give correct results. |
| 189 | + |
| 190 | +Integer division is not supported for SIMD vectors, because noboty uses integer division in optimized code and hardware supports it. |
| 191 | +For multiplying and dividing by powers of two, you can instead pre-calculate the base-two logarithm of the value to multiply or divide. |
| 192 | +Shift left on an unsigned integer to multiply. |
| 193 | +Shift right on an unsigned integer to divide. |
| 194 | + |
| 195 | +Bit shifting is not supported for signed integers, because that would be undefined behavior. |
| 196 | +Some hardware architectures offer signed bit shifting in a way that divides and multiplies signed integers correctly, but these might not even exist for scalar operations. |
| 197 | + |
| 198 | +For optimal performance, use bitShiftLeftImmediate instead of << and bitShiftRightImmediate instead of >> when you know the offset amount in compile time. |
| 199 | +Instead of 'myVector << 5', write 'bitShiftLeftImmediate<5>(myVector)'. |
| 200 | +Instead of 'myVector >> 12', write 'bitShiftRightImmediate<12>(myVector)'. |
| 201 | +Otherwise the hardware abstraction may have to fall back on repeated scalar operations even though bit shifting intrinsic functions are available for immediate offsets. |
| 202 | + |
| 203 | +SIMD vectors with lanes of unsigned integers also support bitwise and (a & b), bitwise inclusive or (a | b), bitwise exclusive or (a ^ b), bitwise negation (~a). |
| 204 | + |
| 205 | +--- |
0 commit comments