Skip to content

Instantly share code, notes, and snippets.

@tannergooding
Created June 17, 2017 18:18
Show Gist options
  • Save tannergooding/89bd72f05ab772bfe5ad3a03d6493650 to your computer and use it in GitHub Desktop.
Save tannergooding/89bd72f05ab772bfe5ad3a03d6493650 to your computer and use it in GitHub Desktop.
System.HashCode implementation for https://github.com/dotnet/corefx/issues/14354
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace System
{
public struct HashCode
{
private int _bytesCombined;
private int _combinedValue;
public void Add<T>(T value)
{
_combinedValue = CombineValue(value.GetHashCode(), _combinedValue);
_bytesCombined += sizeof(int);
}
public void Add<T>(T value, IEqualityComparer<T> comparer)
{
_combinedValue = CombineValue(comparer.GetHashCode(value), _combinedValue);
_bytesCombined += sizeof(int);
}
[Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member
public override int GetHashCode()
#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
{
throw new NotImplementedException();
}
public int ToHashCode()
{
return FinalizeValue(_combinedValue, _bytesCombined);
}
#region Static Methods
// No need to actually instantiate an instance of HashCode for these methods. The number of bytes combined
// is constant per method (sizeof(int) * number of inputs) and the combined value can be a local.
public static int Combine<T1>(T1 value1)
{
return FinalizeValue(value1.GetHashCode(), sizeof(int));
}
public static int Combine<T1, T2>(T1 value1, T2 value2)
{
var combinedValue = CombineValue(value2.GetHashCode(), value1.GetHashCode());
return FinalizeValue(combinedValue, sizeof(int) * 2);
}
public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3)
{
var combinedValue = CombineValue(value2.GetHashCode(), value1.GetHashCode());
combinedValue = CombineValue(value3.GetHashCode(), combinedValue);
return FinalizeValue(combinedValue, sizeof(int) * 3);
}
public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4)
{
var combinedValue = CombineValue(value2.GetHashCode(), value1.GetHashCode());
combinedValue = CombineValue(value3.GetHashCode(), combinedValue);
combinedValue = CombineValue(value4.GetHashCode(), combinedValue);
return FinalizeValue(combinedValue, sizeof(int) * 4);
}
public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5)
{
var combinedValue = CombineValue(value2.GetHashCode(), value1.GetHashCode());
combinedValue = CombineValue(value3.GetHashCode(), combinedValue);
combinedValue = CombineValue(value4.GetHashCode(), combinedValue);
combinedValue = CombineValue(value5.GetHashCode(), combinedValue);
return FinalizeValue(combinedValue, sizeof(int) * 5);
}
public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6)
{
var combinedValue = CombineValue(value2.GetHashCode(), value1.GetHashCode());
combinedValue = CombineValue(value3.GetHashCode(), combinedValue);
combinedValue = CombineValue(value4.GetHashCode(), combinedValue);
combinedValue = CombineValue(value5.GetHashCode(), combinedValue);
combinedValue = CombineValue(value6.GetHashCode(), combinedValue);
return FinalizeValue(combinedValue, sizeof(int) * 6);
}
public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7)
{
var combinedValue = CombineValue(value2.GetHashCode(), value1.GetHashCode());
combinedValue = CombineValue(value3.GetHashCode(), combinedValue);
combinedValue = CombineValue(value4.GetHashCode(), combinedValue);
combinedValue = CombineValue(value5.GetHashCode(), combinedValue);
combinedValue = CombineValue(value6.GetHashCode(), combinedValue);
combinedValue = CombineValue(value7.GetHashCode(), combinedValue);
return FinalizeValue(combinedValue, sizeof(int) * 7);
}
public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8)
{
var combinedValue = CombineValue(value2.GetHashCode(), value1.GetHashCode());
combinedValue = CombineValue(value3.GetHashCode(), combinedValue);
combinedValue = CombineValue(value4.GetHashCode(), combinedValue);
combinedValue = CombineValue(value5.GetHashCode(), combinedValue);
combinedValue = CombineValue(value6.GetHashCode(), combinedValue);
combinedValue = CombineValue(value7.GetHashCode(), combinedValue);
combinedValue = CombineValue(value8.GetHashCode(), combinedValue);
return FinalizeValue(combinedValue, sizeof(int) * 8);
}
#endregion
#region Helper Methods
private static int CombineValue(int value, int seed)
{
var combinedHashCode = value;
unchecked
{
combinedHashCode *= (-862048943);
combinedHashCode = unchecked((int)(RotateLeft(unchecked((uint)(combinedHashCode)), 15)));
combinedHashCode *= 461845907;
combinedHashCode ^= seed;
combinedHashCode = unchecked((int)(RotateLeft(unchecked((uint)(combinedHashCode)), 13)));
combinedHashCode *= 5;
combinedHashCode -= 430675100;
}
return combinedHashCode;
}
public static int FinalizeValue(int combinedValue, int bytesCombined)
{
var finalizedHashCode = combinedValue;
unchecked
{
finalizedHashCode ^= bytesCombined;
finalizedHashCode ^= (int)((uint)(finalizedHashCode) >> 16);
finalizedHashCode *= (-2048144789);
finalizedHashCode ^= (int)((uint)(finalizedHashCode) >> 13);
finalizedHashCode *= (-1028477387);
finalizedHashCode ^= (int)((uint)(finalizedHashCode) >> 16);
}
return finalizedHashCode;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RotateLeft(uint value, byte bits)
{
// This pattern is recognized by the JIT and should be optimized
// to a ROL instruction rather than two shifts.
return unchecked((value << bits) | (value >> (32 - bits)));
}
#endregion
}
}
using System;
using System.Diagnostics;
class Program
{
const int OuterIterations = 10000;
const int InnerIterations = 10000;
static void Main(string[] args)
{
var throughputScale1 = MeasureThroughputScale(TestCombine1);
Console.WriteLine($"1: {throughputScale1 * (sizeof(int) * 1) / 1048576:F2} MB/s");
var throughputScale2 = MeasureThroughputScale(TestCombine2);
Console.WriteLine($"2: {throughputScale2 * (sizeof(int) * 2) / 1048576:F2} MB/s");
var throughputScale3 = MeasureThroughputScale(TestCombine3);
Console.WriteLine($"3: {throughputScale3 * (sizeof(int) * 3) / 1048576:F2} MB/s");
var throughputScale4 = MeasureThroughputScale(TestCombine4);
Console.WriteLine($"4: {throughputScale4 * (sizeof(int) * 4) / 1048576:F2} MB/s");
var throughputScale5 = MeasureThroughputScale(TestCombine5);
Console.WriteLine($"5: {throughputScale5 * (sizeof(int) * 5) / 1048576:F2} MB/s");
var throughputScale6 = MeasureThroughputScale(TestCombine6);
Console.WriteLine($"6: {throughputScale6 * (sizeof(int) * 6) / 1048576:F2} MB/s");
var throughputScale7 = MeasureThroughputScale(TestCombine7);
Console.WriteLine($"7: {throughputScale7 * (sizeof(int) * 7) / 1048576:F2} MB/s");
var throughputScale8 = MeasureThroughputScale(TestCombine8);
Console.WriteLine($"8: {throughputScale8 * (sizeof(int) * 8) / 1048576:F2} MB/s");
}
static double MeasureThroughputScale(Func<(double elapsed, int result)> func)
{
var elapsed = 0.0;
func(); // Cold Test so that JIT overhead doesn't add too much noise
for (int i = 0; i < OuterIterations; i++)
{
elapsed += func().elapsed;
}
var outerIterationTime = elapsed / OuterIterations;
var innerIterationTime = outerIterationTime / InnerIterations;
return (1.0 / innerIterationTime);
}
#region Tests
// We need to actually consume the output and modify the inputs to ensure the JIT doesn't perform
// dead-code optimization (I've not actually seen it do this outside of intrinsics, but better safe
// than sorry).
// Additionally, we just pass the previous result as the inputs for the next call. This simulates
// the values changing between each iteration but should also avoid too much noise from memory
// reads (as the result should be stored in RAX and is just copied to other registers or the local
// stack for the next call).
static (double, int) TestCombine1()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
static (double, int) TestCombine2()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result, result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
static (double, int) TestCombine3()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result, result, result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
static (double, int) TestCombine4()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result, result, result, result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
static (double, int) TestCombine5()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result, result, result, result, result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
static (double, int) TestCombine6()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result, result, result, result, result, result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
static (double, int) TestCombine7()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result, result, result, result, result, result, result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
static (double, int) TestCombine8()
{
var result = 0;
var start = Stopwatch.GetTimestamp();
for (int i = 0; i< InnerIterations; i++)
{
result = HashCode.Combine(result, result, result, result, result, result, result, result);
}
var stop = Stopwatch.GetTimestamp();
var elapsed = (stop - start) / (double)(Stopwatch.Frequency);
return (elapsed, result);
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment