Click here to Skip to main content
15,896,912 members
Articles / Programming Languages / C#

The List Trifecta, Part 1

Rate me:
Please Sign up or sign in to vote.
4.97/5 (21 votes)
20 May 2016LGPL321 min read 37.1K   161   40  
The A-list is an all-purpose list, a data structure that can support most standard list operation in O(log n) time and does lots of other stuff, too
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace Loyc.Math
{
	/// <summary>Contains methods for manipulating 128-bit numbers, multiplying 
	/// 64-bit numbers to produce 128-bit results, and dividing 128-bit numbers 
	/// by 64-bit numbers.</summary>
	/// <remarks>
	/// MathEx.MulDiv and MathEx.MulShift rely on this class for 64x64 MulDiv() 
	/// and MulShift().
	/// <para/>
	/// Some functionality is incomplete at this time, e.g. there are no 
	/// comparison methods or 128+128 addition/subtraction.
	/// <para/>
	/// This class works on raw Int64 values, so a 128-bit number is represented 
	/// by a pair of Int64s, one with high bits and one with low bits. By 
	/// convention, the low 64 bits are always unsigned (because they contain no
	/// sign bit), and the high 64 bits may be signed or unsigned, which 
	/// represents the sign of the 128-bit number as a whole.
	/// <para/>
	/// Many methods pass the low 64 bits by reference and the high 64 bits by 
	/// value, returning the "new" high 64 bits as the return value. This is done 
	/// to help implement signed math in terms of unsigned math. If the parameter
	/// type is "ref ulong", C# does not allow you to pass "ref long", even with
	/// a cast. Passing the high bits by value is therefore less cumbersome.
	/// </remarks>
	public static class Math128
	{
		#region Multiplication

		public static ulong Multiply(ulong a, ulong b, out ulong resultHi)
		{
			uint aH = (uint)(a >> 32), aL = (uint)a;
			uint bH = (uint)(b >> 32), bL = (uint)b;
			// Multiply a*b: (aH*EXP + aL) * (bH*EXP + bL), where EXP=1<<32
			//   Expand a*b: aH*bH*EXP*EXP + (aL*bH + aH*bL)*EXP + aL*bL
			//    High part: aH*bH
			//     Mid part: aL*bH + aH*bL
			//     Low part: aL*bL
			ulong mid1 = (ulong)aL * bH;
			ulong mid  = (ulong)aH * bL + mid1;
			resultHi = (ulong)aH * bH + (mid >> 32);
			ulong lo1 = (ulong)aL * bL;
			ulong lo = lo1 + (mid << 32);
			
			if (mid < mid1)
				resultHi += (1 << 32); // mid (aL * bH + aH * bL) overflowed
			if (lo < lo1)
				resultHi++; // (aL * bL) + (lower half of mid << 32) overflowed
			return lo;
		}

		public static ulong Multiply(long a, long b, out long resultHi)
		{
			bool negative;
			if ((negative = (a < 0)))
				a = -a;
			if (b < 0) {
				b = -b;
				negative = !negative;
			}
			ulong resultHiU, resultLo = Multiply((ulong)a, (ulong)b, out resultHiU);
			resultHi = (long)resultHiU;
			if (negative)
				Negate(ref resultHi, ref resultLo);
			return resultLo;
		}

		#endregion

		#region Division (the really difficult one)

		/// <summary>Divides an signed 128-bit number by a signed 64-bit 
		/// number to produce a 64-bit result.</summary>
		/// <returns>Returns the division result (known as the "quotient"). If the 
		/// result is too large to represent, long.MinValue or long.MaxValue is 
		/// returned instead.</returns>
		/// <inheritdoc cref="Divide(long, ulong, long, out long, out long, bool)"/>
		public static long Divide(long aH, ulong aL, long b, out long remainder, bool roundDown)
		{
			long resultHi;
			ulong resultLo = Divide(aH, aL, b, out resultHi, out remainder, roundDown);
			if (resultHi == ((long)resultLo >= 0 ? 0 : -1L))
				return (long)resultLo;
			else
				// result does not fit in 64 bits
				return resultHi >= 0 ? long.MaxValue : long.MinValue;
		}
		
		/// <summary>Divides an signed 128-bit number by a signed 64-bit 
		/// number to produce a 128-bit result.</summary>
		/// <param name="roundDown">If true, the result is rounded down, instead
		/// of being rounded toward zero, which changes the remainder and 
		/// quotient if a is negative but b is positive.</param>
		/// <remarks>
		/// When dividing a negative number by a positive number, it is 
		/// conventionally rounded toward zero. Consequently, the remainder is zero 
		/// or negative to satisfy the standard integer division equation 
		/// <c>a = b*q + r</c>, where q is the quotient (result) and r is the 
		/// remainder.
		/// <para/>
		/// This is not always what you want. So, if roundDown is true, the result 
		/// is rounded down instead of toward zero. This ensures that if 'a' is 
		/// negative and 'b' is positive, the remainder is positive too, a fact 
		/// which is useful for modulus arithmetic. The table below illustrates 
		/// the difference:
		/// <pre>
		/// inputs   | standard  | roundDown
		///          |  output   |  output  
		///  a   b   |   q   r   |   q   r
		/// --- ---  |  --- ---  |  --- ---
		///  7   3   |   2   1   |   2   1
		/// -7   3   |  -2  -1   |  -3   2
		///  7  -3   |  -2   1   |  -3  -2
		/// -7  -3   |   2  -1   |   2  -1
		/// </pre>
		/// </remarks>
		/// <inheritdoc cref="Divide(ulong, ulong, ulong, out ulong, out ulong)"/>
		public static ulong Divide(long aH, ulong aL, long b, out long resultHi, out long remainder, bool roundDown)
		{
			bool negativeA, negativeB;
			if ((negativeA = (aH < 0)))
				Negate(ref aH, ref aL);
			if ((negativeB = (b < 0)))
				b = -b;
			
			ulong resultHiU, remainderU;
			ulong resultLo = Divide((ulong)aH, aL, (ulong)b, out resultHiU, out remainderU);
			resultHi = (long)resultHiU;
			if (negativeA == negativeB) {
				remainder = (negativeA ? -(long)remainderU : (long)remainderU);
				return resultLo;
			} else if (roundDown && remainderU != 0) {
				remainderU = (ulong)b - remainderU;
				remainder = (negativeB ? -(long)remainderU : (long)remainderU);
				// ~result is equivalent to (-result - 1)
				resultHi = ~resultHi;
				resultLo = ~resultLo;
				return resultLo;
			} else {
				remainder = (negativeA ? -(long)remainderU : (long)remainderU);
				Negate(ref resultHi, ref resultLo);
				return resultLo;
			}
		}

		/// <summary>Divides an signed 128-bit number by a signed 64-bit 
		/// number to produce a 64-bit result.</summary>
		/// <remarks>If the result did not fit in 64 bits, this method returns 
		/// ulong.MaxValue.</remarks>
		/// <inheritdoc cref="Divide(ulong, ulong, ulong, out ulong, out ulong)"/>
		public static ulong Divide(ulong aH, ulong aL, ulong b, out ulong remainder)
		{
			if (b <= aH) // overflow
				return (remainder = ulong.MaxValue);

			ulong resultHi, resultLo = Divide(aH, aL, b, out resultHi, out remainder);
			if (resultHi != 0)
				return ulong.MaxValue;
			return resultLo;
		}

		/// <summary>Divides an unsigned 128-bit number by an unsigned 64-bit 
		/// number to produce a 128-bit result.</summary>
		/// <param name="aH">High 64 bits of the dividend.</param>
		/// <param name="aL">Low 64 bits of the dividend.</param>
		/// <param name="b">The divisor.</param>
		/// <param name="resultHi">High 64 bits of result.</param>
		/// <param name="remainder">Remainder of the division.</param>
		/// <returns>The low 64 bits of the result (known as the "quotient").</returns>
		/// <exception cref="DivideByZeroException">b was zero.</exception>
		public static ulong Divide(ulong aH, ulong aL, ulong b, out ulong resultHi, out ulong remainder)
		{
			if (aH == 0)
			{
				// 64/64-bit division: use compiler intrinsic
				resultHi = 0;
				remainder = aL % b;
				return aL / b;
			}
			if ((b >> 32) == 0)
			{
				// 128/32-bit division
				uint bL = (uint)b;

				// Optimize for b=1 and b=2
				if (bL <= 2)
				{
					if (bL == 2) {
						remainder = aL & 1;
						resultHi = ShiftRightFast(aH, ref aL, 1);
						return aL;
					}
					if (bL == 1) {
						remainder = 0;
						resultHi = aH;
						return aL;
					}
					throw new DivideByZeroException();
				}

				uint a4;
				ulong a3, a2, a1;
				uint result4, result3, result2, result1;
				uint r;

				// There are obvious machine-language optimizations here...
				// I hope the JIT is smart enough to see them.
				a4 = (uint)(aH >> 32);
				if (a4 == 0)
					r = result4 = 0;
				else {
					result4 = a4 / bL;
					r = a4 % bL;
				}

				a3 = ((ulong)r << 32) + (uint)aH;
				result3 = (uint)(a3 / bL);
				r = (uint)(a3 % bL);

				a2 = ((ulong)r << 32) + (uint)(aL >> 32);
				result2 = (uint)(a2 / bL);
				r = (uint)(a2 % bL);

				a1 = ((ulong)r << 32) + (uint)aL;
				result1 = (uint)(a1 / bL);
				r = (uint)(a1 % bL);

				resultHi = (ulong)(result4 << 32) + result3;
				remainder = r;
				return (ulong)(result2 << 32) + result1;
			}
			else
			{
				Debug.Assert(aH != 0);
				int iterations = 128;

				// Optimization 1: skip loop iterations that have no effect
				if ((aH >> 32) == 0) {
					aH = ShiftLeftFast(aH, ref aL, 32);
					iterations -= 32;
				}
				if (aH < (1 << (64-16))) {
					aH = ShiftLeftFast(aH, ref aL, 16);
					iterations -= 16;
				}
				if (aH < (1 << (64-8))) {
					aH = ShiftLeftFast(aH, ref aL, 8);
					iterations -= 8;
				}
				if (aH < (1 << (64-4))) {
					aH = ShiftLeftFast(aH, ref aL, 4);
					iterations -= 4;
				}
				if (aH < (1 << (64-2))) {
					aH = ShiftLeftFast(aH, ref aL, 2);
					iterations -= 2;
				}

				// Optimization 2: get a head start by shifting some bits into 
				// 'remainder', but not enough to change the outcome.
				Debug.Assert(b > uint.MaxValue);
				int skip = MathEx.Log2Floor((uint)(b >> 32)) + 32;
				iterations -= skip;
				remainder = ShiftLeftEx(ref aH, ref aL, skip);

				// The core division algorithm is based on the assembly code in 
				// http://www.codeproject.com/KB/recipes/MulDiv64.aspx
				// Unoptimized, it required an iteration for every bit of the input 
				// (a). The way it works is slightly subtle. The dividend 'a' 
				// slowly becomes the output as the loop progresses. The original 
				// bits of 'a' are shifted left one-by-one into 'remainder', and 'a'
				// is shifted 128 times so it eventually disappears. Meanwhile, the 
				// bits of the result are determined one-at-a-time and are shifted 
				// in as the new low bits of 'a'. In general, this is more efficient 
				// than using separate variables for the dividend and the result.
				Debug.Assert(remainder < b);
				for (; iterations != 0; iterations--) {
					ulong oldH = aH, oldL = aL, oldR = remainder;
					remainder <<= 1;
					aH <<= 1;
					if (aH < oldH) // aH overflowed?
						++remainder;
					aL <<= 1;
					if (aL < oldL) // aL overflowed?
						++aH;
					if (remainder < oldR || remainder >= b)
					{
						remainder -= b;
						if (++aL == 0) // aL overflowed?
							++aH;
					}
				}

				resultHi = aH;
				return aL;
			}
		}

		#endregion

		#region Shift left

		/// <summary>Shifts a 128-bit value left.</summary>
		/// <param name="aH">High 64 bits</param>
		/// <param name="aL">Low 64 bits</param>
		/// <param name="amount">Number of bits to shift.</param>
		/// <returns>The new value of aH</returns>
		/// <remarks>The convention is that signed numbers use Int64 for aH and 
		/// unsigned numbers used UInt64 for aH. The fact that aH is not passed 
		/// by reference makes it easier to shift a signed number left by casting 
		/// aH to UInt64. The cast would not be allowed if passing by reference.
		/// Of course, right shift, on the other hand, requires two separate 
		/// methods since it behaves differently for signed and unsigned inputs.
		/// <para/>
		/// This method does not allow shifting by a negative amount. The reason 
		/// is that there is only one ShiftLeft, so if the amount is negative, 
		/// it's not known whether a signed or unsigned ShiftRight is intended.
		/// </remarks>
		public static ulong ShiftLeft(ulong aH, ref ulong aL, int amount)
		{
			Debug.Assert(amount >= 0);
			if (amount < 64)
				return ShiftLeftFast(aH, ref aL, amount);
			else {
				if (amount >= 128)
					aH = 0;
				else
					aH = aL << (amount - 64);
				aL = 0;
			}
			return aH;
		}
		
		/// <summary>Variation of ShiftLeft() for cases when you know 64 > amount >= 0.</summary>
		public static ulong ShiftLeftFast(ulong aH, ref ulong aL, int amount)
		{
			Debug.Assert((uint)amount < 64u);
			aH = (aH << amount) + (aL >> (64-amount));
			aL <<= amount;
			return aH;
		}

		/// <summary>Shifts a 128-bit value left and saves the overflowed bits.</summary>
		/// <param name="aH">High 64 bits</param>
		/// <param name="aL">Low 64 bits</param>
		/// <param name="overflow">The bits shifted off the left of aH are <i>added
		/// </i> to overflow. If overflow is an alias for aL, a left rotation 
		/// occurs.</param>
		/// <param name="amount">Number of bits to shift. Negative amounts are not permitted.</param>
		public static ulong ShiftLeftEx(ref ulong aH, ref ulong aL, int amount)
		{
			Debug.Assert(amount >= 0);
			ulong overflow;
			if (amount < 64) {
				ulong newL = (aL << amount);
				ulong newH = (aH << amount) + (aL >> (64-amount));
				overflow =                    (aH >> (64-amount));
				aL = newL;
				aH = newH;
			} else if (amount < 128) {
				overflow = aL >> (128-amount);
				aH = aL << (amount-64);
				aL = 0;
			} else if (amount < 192) {
				overflow = (aL << (amount-128));
				aH = aL = 0;
			} else {
				aH = aL = 0;
				return 0;
			}
			return overflow;
		}
		public static ulong RotateLeft(ulong aH, ref ulong aL, int amount)
		{
			aL += ShiftLeftEx(ref aH, ref aL, amount);
			return aH;
		}
		
		#endregion
		
		#region Shift right

		/// <summary>Shifts a 128-bit value right.</summary>
		/// <param name="aH">High 64 bits</param>
		/// <param name="aL">Low 64 bits</param>
		/// <param name="amount">Number of bits to shift.</param>
		/// <returns>The new value of aH</returns>
		/// <remarks>This method, unlike ShiftLeft(), allows shifting by a negative 
		/// amount, which is translated to a left shift.
		/// <para/>
		/// TODO: ShiftRightEx</remarks>
		public static ulong ShiftRight(ulong aH, ref ulong aL, int amount)
		{
			if (amount < 0)
				return ShiftLeft(aH, ref aL, -amount);
			if (amount < 64)
				return ShiftRightFast(aH, ref aL, amount);
			else {
				if (amount >= 128)
					aL = 0;
				else
					aL = aH >> (amount - 64);
				aH = 0;
			}
			return aH;
		}

		/// <summary>Variation of ShiftRight() for cases when you know 64 > amount >= 0.</summary>
		private static ulong ShiftRightFast(ulong aH, ref ulong aL, int amount)
		{
			Debug.Assert((uint)amount < 64u);
			aL = (aL >> amount) + (aH << (64-amount));
			return aH >> amount;
		}

		/// <inheritdoc cref="ShiftRight(ulong, ref ulong, int)">
		public static long ShiftRight(long aH, ref ulong aL, int amount)
		{
			if (amount < 0)
				return (long)ShiftLeft((ulong)aH, ref aL, -amount);
			if (amount < 64)
				return ShiftRightFast(aH, ref aL, amount);
			else if (amount < 127) {
				aL = (ulong)(aH >> (amount - 64));
				aH >>= 63; // keep only the sign
			} else
				aL = (ulong)(aH >>= 63);
			return aH;
		}

		/// <summary>Variation of ShiftRight() for cases when you know 64 > amount >= 0.</summary>
		private static long ShiftRightFast(long aH, ref ulong aL, int amount)
		{
			Debug.Assert((uint)amount < 64u);
			aL = (aL >> amount) + ((ulong)aH << (64-amount));
			return aH >> amount;
		}

		#endregion

		#region Addition and subtraction

		/// <summary>Adds a 64-bit number to a 128-bit number.</summary>
		/// <param name="aH">High 64 bits of 128-bit number</param>
		/// <param name="aL">Low 64 bits of 128-bit number</param>
		/// <param name="amount">Amount to add</param>
		/// <returns>The high 64 bits of the result.</returns>
		public static ulong Add(ulong aH, ref ulong aL, ulong amount)
		{
			ulong oldL = aL;
			aL += amount;
			if (oldL > aL) // 64-bit overflow
				aH++;
			return aH;
		}
		
		/// <summary>Subtracts a 64-bit number from a 128-bit number.</summary>
		/// <param name="aH">High 64 bits of 128-bit number</param>
		/// <param name="aL">Low 64 bits of 128-bit number</param>
		/// <param name="amount">Amount to subtract</param>
		/// <returns>The high 64 bits of the result.</returns>
		public static ulong Subtract(ulong aH, ref ulong aL, ulong amount)
		{
			ulong oldL = aL;
			aL -= amount;
			if (aL > oldL) // 64-bit undeflow
				aH--;
			return aH;
		}

		/// <inheritdoc cref="Add(ulong, ref ulong, ulong)"/>
		public static long Add(long aH, ref ulong aL, long amount)
		{
			if (amount >= 0)
				return (long)Add((ulong)aH, ref aL, (ulong)amount);
			else
				return (long)Subtract((ulong)aH, ref aL, (ulong)(-amount));
		}
		
		/// <inheritdoc cref="Subtract(ulong, ref ulong, ulong)"/>
		public static long Subtract(long aH, ref ulong aL, long amount)
		{
			if (amount >= 0)
				return (long)Subtract((ulong)aH, ref aL, (ulong)amount);
			else
				return (long)Add((ulong)aH, ref aL, (ulong)(-amount));
		}

		#endregion

		#region Increment and other simple operations

		private static ulong Increment(ulong resultHi, ref ulong resultLo)
		{
			if (++resultLo == 0)
				++resultHi;
			return resultHi;
		}
		private static ulong Decrement(ulong resultHi, ref ulong resultLo)
		{
			if ((long)--resultLo == -1L)
				--resultHi;
			return resultHi;
		}
		public static void Negate(ref long aH, ref ulong aL)
		{
			aH = ~aH;
			if ((aL = (ulong)(-(long)aL)) == 0)
				aH++;
		}
		public static long SignExtend(long a)
		{
			if (a < 0) a = -1;
			return a;
		}

		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Software Developer None
Canada Canada
Since I started programming when I was 11, I wrote the SNES emulator "SNEqr", the FastNav mapping component, the Enhanced C# programming language (in progress), the parser generator LLLPG, and LES, a syntax to help you start building programming languages, DSLs or build systems.

My overall focus is on the Language of your choice (Loyc) initiative, which is about investigating ways to improve interoperability between programming languages and putting more power in the hands of developers. I'm also seeking employment.

Comments and Discussions