What guarantees does System.Numerics.Vectors provide about size and bit order?

Viewed 200

I have implemented a vector-based c# approximation of Log. It includes unsafe code. It's been working fine in a number of environments, but on a recent deployment has fallen over. The implementation uses SIMD through the System.Numerics.Vectors library.

Unfortunately I cannot test on the system where the software isn't working. However, I would like to know which assumptions I made about the library are invalid:

  • Does Vector<float>.Count always return a power of 2 ?
  • Does Vector<UInt64>.Count == Vector.Count * 2 ?
  • Can I take a pointer of a vector using Unsafe.AsPointer, then perform standard operations as though it were N packed numbers in memory ?
  • Are there any processors that dotNet 4 runs on that have different endian-ness or don't store floats in IEEE754 format?

The code is as follows:

const float invLn2 = 1.44269504089f; // 1 / ln(2)
        const float pow2_126 = 8.5070592e+37f; //2^126

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Vector<float> QuickLog2(Vector<float> vecOrig)
        {
            //32 bit Float specification:
            //Leftmost bit is sign bit.
            //Next 8 bits are exponent
            //Next 23 bits are mantissa
            unsafe
            {
                var ints = Vector.AsVectorUInt32(vecOrig);

                var exponents = Vector.BitwiseAnd(ints, new Vector<uint>(0x7F800000));
                BitshiftVector23(Unsafe.AsPointer(ref exponents));

                var unsignedExponents = exponents - new Vector<uint>(127);
                var signedExponents = Vector.AsVectorInt32(unsignedExponents);
                var localMantissBitmask = Vector.AsVectorSingle(new Vector<UInt32>(0x807FFFFF));
                var maskedMantissas = Vector.BitwiseAnd(vecOrig, localMantissBitmask);
                var mantissas = maskedMantissas * new Vector<float>(pow2_126);

                var mantissasLogged = LogPolynomialFunction2(mantissas) * new Vector<float>(invLn2);

                Vector<float> floatExponents;
#if false
                floatExponents = Vector.ConvertToSingle(signedExponents);               
#else
                ConvertIntToFloatInPace(Unsafe.AsPointer(ref signedExponents));
                floatExponents = Vector.AsVectorSingle(signedExponents);
#endif

                return mantissasLogged + floatExponents;
            }
        }

        const float log10_2 = 0.30102999566398119521373889472449f;
        /// <summary>
        /// A vectorized implementation of Log10(N). Uses bitshift, bitmasks, and unsafe code.
        /// Does not have the same safety as Math.Log10: Behaviour for infities, zero, negative numbers are undefined.
        /// </summary>
        /// <param name="vec">The vector to take the log of</param>
        /// <returns>The log, to the base 10, of the vector</returns>
        /// <remarks>
        /// Accurate to about 10^-7, which is the limit of a 32 bit float anyway.
        /// In my (BS) tests, takes about twice as long to run on as Math.Log10(...), but operates on 8 numbers,
        /// so 4x faster.
        /// Reverts to Math.Log10(...) if vectors are not hardware accelerated. 
        /// Given the extra memory copies required, that will be much slower than using scalar code.
        /// It'll be nice once intrinsics make it into dotNet and we can replace this with a single instruction...
        /// </remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Vector<float> QuickLog10(Vector<float> vec)
        {
            if (Vector.IsHardwareAccelerated)
                return QuickLog2(vec) * new Vector<float>(log10_2);
            else
            {
                float[] tmp = new float[Vector<float>.Count];
                vec.CopyTo(tmp);
                for (int i = 0; i < Vector<float>.Count; i++)
                    tmp[i] = (float)Math.Log10(tmp[i]);
                return new Vector<float>(tmp);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe void BitshiftVector23(void* vector)
        {
            UInt64* asUlong = (UInt64*)vector;
            if (Vector<UInt64>.Count == 4)
            {
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
            }
            else if (Vector<UInt64>.Count == 8)
            {
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
                asUlong++;
                *asUlong = *asUlong >> 23;
            }
            else
                for (int i = 0; i < Vector<UInt64>.Count; i++)
                    asUlong[i] = asUlong[i] >> 23;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe void ConvertIntToFloatInPace(void* vector)
        {
            int* asInt = (int*)vector;
            if (Vector<int>.Count == 8)
            {
                *(float*)asInt = *asInt;
                asInt++;
                *(float*)asInt = *asInt;
                asInt++;
                *(float*)asInt = *asInt;
                asInt++;
                *(float*)asInt = *asInt;
                asInt++;
                *(float*)asInt = *asInt;
                asInt++;
                *(float*)asInt = *asInt;
                asInt++;
                *(float*)asInt = *asInt;
                asInt++;
                *(float*)asInt = *asInt;
                asInt++;
            }
            else if (Vector<UInt64>.Count == 16)
            {
                for (int i = 0; i < 2; i++)
                {
                    *(float*)asInt = *asInt;
                    asInt++;
                    *(float*)asInt = *asInt;
                    asInt++;
                    *(float*)asInt = *asInt;
                    asInt++;
                    *(float*)asInt = *asInt;
                    asInt++;
                    *(float*)asInt = *asInt;
                    asInt++;
                    *(float*)asInt = *asInt;
                    asInt++;
                    *(float*)asInt = *asInt;
                    asInt++;
                    *(float*)asInt = *asInt;
                    asInt++;
                }
            }
            else
                for (int i = 0; i < Vector<UInt64>.Count; i++)
                {
                    *(float*)asInt = *asInt;
                    asInt++;
                }
        }


        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static Vector<float> LogPolynomialFunction2(Vector<float> mantissas)
        {
            var zm1 = mantissas;
            var zp1 = mantissas + new Vector<float>(2f);
            var zm1Divzp1 = Vector.Divide(zm1, zp1);
            var squared = zm1Divzp1 * zm1Divzp1;
            var cur = zm1Divzp1;

            //Manual loop unwinding:
#if false
                var mantissasLogged = Vector<float>.Zero;
                for (float i = 0; i < 4; i++)
                {
                    var fac = 2f / (2f * i + 1f);
                    mantissasLogged += cur * new Vector<float>(fac);
                    cur *= squared;
                }
#else
            //i = 0;
            const float fac0 = 2f / (2 * 0 + 1);
            var mantissasLogged = cur * new Vector<float>(fac0);
            cur *= squared;

            //i = 1;
            const float fac1 = 2f / (2 * 1 + 1);
            mantissasLogged += cur * new Vector<float>(fac1);
            cur *= squared;

            //i = 2;
            const float fac2 = 2f / (2 * 2 + 1);
            mantissasLogged += cur * new Vector<float>(fac2);
            cur *= squared;

            //i = 3;
            const float fac3 = 2f / (2 * 3 + 1);
            mantissasLogged += cur * new Vector<float>(fac3);
            cur *= squared;

            //i = 4;
            const float fac4 = 2f / (2 * 4 + 1);
            mantissasLogged += cur * new Vector<float>(fac4);
#endif
            return mantissasLogged;
        }

EDIT: I put some simple tests into the program on startup. Vector.IsHardwareAccelerated == true; Vector.Count == 4; This vectorised Log gives the correct answer for the first two inputs, but incorrect for the second two. Perhaps the assumption that Unsafe.AsPointer(Vector) gives me a pointer to the vector elements as four consecutive floats is incorrect.

Log outputs:

DEBUG Vector.IsHardwareAccelerated: True 
DEBUG Vector<float>.Count: 4 
DEBUG Vector<Uint64>.Count: 2 
DEBUG MathUtils test input data: 5.967E+009,1.072E+006,9.521E+017,4.726E+000 
DEBUG MathUtils required output: 9.776,6.030,17.979,0.674 
DEBUG MathUtils actual output: 9.776,6.030,0.218,0.072 

(Yet to have a chance to check the bit patterns...)

1 Answers

IEEE 754 floating-point standard does not specify endianness, it definitely could be a problem here (depending on what you are running on)

You can use BitConverter.IsLittleEndian and vary accordingly

Indicates the byte order ("endianness") in which data is stored in this computer architecture.

Related