Hash Algorithm Performance
Sooner or later you’ll need to use a cryptographic hash function. Sometimes it’s to quickly check if two large byte arrays are the same, sometimes it’s for interoperability with some server, and other times it’s to obfuscate a string. In any case, performance of the various hash algorithms varies wildly. Today’s article performance tests all 27 hash algorithm permutations to see which is fastest and which is slowest. Read on for the performance test results!
Unity currently supports a good number of hash algorithms:
- MD5
- MD5CryptoServiceProvider
- RIPEMD160Managed
- RIPEMD160
- SHA1Managed
- SHA1CryptoServiceProvider
- SHA256Managed
- SHA256
- SHA384Managed
- SHA384
- SHA512Managed
- SHA512
And many more that use a key you provide, similar to seeding a random number generator:
- HMACMD5
- HMACRIPEMD160
- MACTripleDES
- HMACSHA1
- HMACSHA256
- HMACSHA384
- HMACSHA512
- HMACMD5
- HMACRIPEMD160
- MACTripleDES
- HMACSHA1
- HMACSHA1
- HMACSHA256
- HMACSHA384
- HMACSHA512
To test them, I ran two tests for each. In the first test, I created 1000 instances of the hash algorithm class. In the second, I hashed a 1 MB byte array. Here’s the test:
using System; using System.Security.Cryptography; using UnityEngine; public class TestScript : MonoBehaviour { private string report = string.Empty; private System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); private static readonly byte[] DataToHash = new byte[1024*1024]; private const int NumCreateIterations = 1000; private const int NumHashIterations = 1; long TimeOperation(int numIterations, Action action) { stopwatch.Reset(); stopwatch.Start(); for (var i = 0; i < numIterations; ++i) { action(); } return stopwatch.ElapsedMilliseconds; } long TimeCreate(Action action) { return TimeOperation(NumCreateIterations, action); } long TimeHash(HashAlgorithm hashAlgorithm) { return TimeOperation(NumHashIterations, () => hashAlgorithm.ComputeHash(DataToHash)); } void Start() { var random = new System.Random(123); var bytes64 = new byte[64]; var bytes16 = new byte[16]; random.NextBytes(bytes64); random.NextBytes(bytes16); MD5 md5 = null; MD5CryptoServiceProvider md5csp = null; RIPEMD160Managed ripeMd160Managed = null; RIPEMD160 ripeMd160 = null; SHA1Managed sha1managed = null; SHA1CryptoServiceProvider sha1csp = null; SHA256Managed sha256managed = null; SHA256 sha256 = null; SHA384Managed sha384managed = null; SHA384 sha384 = null; SHA512Managed sha512managed = null; SHA512 sha512 = null; HMACMD5 hmacMd5 = null; HMACRIPEMD160 hmacRipeMd160 = null; MACTripleDES macTripleDes = null; HMACSHA1 hmacSha1 = null; HMACSHA256 hmacSha256 = null; HMACSHA384 hmacSha384 = null; HMACSHA512 hmacSha512 = null; HMACMD5 hmacMd5Keyed = null; HMACRIPEMD160 hmacRipeMd160Keyed = null; MACTripleDES macTripleDesKeyed = null; HMACSHA1 hmacSha1Keyed = null; HMACSHA1 hmacSha1KeyedManaged = null; HMACSHA256 hmacSha256Keyed = null; HMACSHA384 hmacSha384Keyed = null; HMACSHA512 hmacSha512Keyed = null; var createMd5 = TimeCreate(() => md5 = MD5.Create()); var createMd5Csp = TimeCreate(() => md5csp = new MD5CryptoServiceProvider()); var createRipeMd160Managed = TimeCreate(() => ripeMd160Managed = new RIPEMD160Managed()); var createRipeMd160 = TimeCreate(() => ripeMd160 = RIPEMD160.Create()); var createSha1Managed = TimeCreate(() => sha1managed = new SHA1Managed()); var createSha1Csp = TimeCreate(() => sha1csp = new SHA1CryptoServiceProvider()); var createSha256Managed = TimeCreate(() => sha256managed = new SHA256Managed()); var createSha256 = TimeCreate(() => sha256 = SHA256.Create()); var createSha384Managed = TimeCreate(() => sha384managed = new SHA384Managed()); var createSha384 = TimeCreate(() => sha384 = SHA384.Create()); var createSha512Managed = TimeCreate(() => sha512managed = new SHA512Managed()); var createSha512 = TimeCreate(() => sha512 = SHA512.Create()); var createHmacMd5 = TimeCreate(() => hmacMd5 = new HMACMD5()); var createHmacRipeMd160 = TimeCreate(() => hmacRipeMd160 = new HMACRIPEMD160()); var createHmacTripleDes = TimeCreate(() => macTripleDes = new MACTripleDES()); var createHmacSha1 = TimeCreate(() => hmacSha1 = new HMACSHA1()); var createHmacSha256 = TimeCreate(() => hmacSha256 = new HMACSHA256()); var createHmacSha384 = TimeCreate(() => hmacSha384 = new HMACSHA384()); var createHmacSha512 = TimeCreate(() => hmacSha512 = new HMACSHA512()); var createHmacMd5Keyed = TimeCreate(() => hmacMd5Keyed = new HMACMD5(bytes64)); var createHmacRipeMd160Keyed = TimeCreate(() => hmacRipeMd160Keyed = new HMACRIPEMD160(bytes64)); var createHmacTripleDesKeyed = TimeCreate(() => macTripleDesKeyed = new MACTripleDES(bytes16)); var createHmacSha1Keyed = TimeCreate(() => hmacSha1Keyed = new HMACSHA1(bytes64, false)); var createHmacSha1KeyedManaged = TimeCreate(() => hmacSha1KeyedManaged = new HMACSHA1(bytes64, true)); var createHmacSha256Keyed = TimeCreate(() => hmacSha256Keyed = new HMACSHA256(bytes64)); var createHmacSha384Keyed = TimeCreate(() => hmacSha384Keyed = new HMACSHA384(bytes64)); var createHmacSha512Keyed = TimeCreate(() => hmacSha512Keyed = new HMACSHA512(bytes64)); var hashMd5 = TimeHash(md5); var hashMd5Csp = TimeHash(md5csp); var hashRipeMd160Managed = TimeHash(ripeMd160Managed); var hashRipeMd160 = TimeHash(ripeMd160); var hashSha1Managed = TimeHash(sha1managed); var hashSha1Csp = TimeHash(sha1csp); var hashSha256Managed = TimeHash(sha256managed); var hashSha256 = TimeHash(sha256); var hashSha384Managed = TimeHash(sha384managed); var hashSha384 = TimeHash(sha384); var hashSha512Managed = TimeHash(sha512managed); var hashSha512 = TimeHash(sha512); var hashHmacMd5 = TimeHash(hmacMd5); var hashHmacRipeMd160 = TimeHash(hmacRipeMd160); var hashHmacTripleDes = TimeHash(macTripleDes); var hashHmacSha1 = TimeHash(hmacSha1); var hashHmacSha256 = TimeHash(hmacSha256); var hashHmacSha384 = TimeHash(hmacSha384); var hashHmacSha512 = TimeHash(hmacSha512); var hashHmacMd5Keyed = TimeHash(hmacMd5Keyed); var hashHmacRipeMd160Keyed = TimeHash(hmacRipeMd160Keyed); var hashHmacTripleDesKeyed = TimeHash(macTripleDesKeyed); var hashHmacSha1Keyed = TimeHash(hmacSha1Keyed); var hashHmacSha1KeyedManaged = TimeHash(hmacSha1KeyedManaged); var hashHmacSha256Keyed = TimeHash(hmacSha256Keyed); var hashHmacSha384Keyed = TimeHash(hmacSha384Keyed); var hashHmacSha512Keyed = TimeHash(hmacSha512Keyed); report = "Hash Algorithm,1000x Create Time,1MB Hash Time\n" + "MD5," + createMd5 + "," + hashMd5 + "\n" + "MD5CryptoServiceProvider," + createMd5Csp + "," + hashMd5Csp + "\n" + "RIPEMD160Managed," + createRipeMd160Managed + "," + hashRipeMd160Managed + "\n" + "RIPEMD160," + createRipeMd160 + "," + hashRipeMd160 + "\n" + "SHA1Managed," + createSha1Managed + "," + hashSha1Managed + "\n" + "SHA1CryptoServiceProvider," + createSha1Csp + "," + hashSha1Csp + "\n" + "SHA256Managed," + createSha256Managed + "," + hashSha256Managed + "\n" + "SHA256," + createSha256 + "," + hashSha256 + "\n" + "SHA384Managed," + createSha384Managed + "," + hashSha384Managed + "\n" + "SHA384," + createSha384 + "," + hashSha384 + "\n" + "SHA512Managed," + createSha512Managed + "," + hashSha512Managed + "\n" + "SHA512," + createSha512 + "," + hashSha512 + "\n" + "HMACMD5," + createHmacMd5 + "," + hashHmacMd5 + "\n" + "HMACRIPEMD160," + createHmacRipeMd160 + "," + hashHmacRipeMd160 + "\n" + "MACTripleDes," + createHmacTripleDes + "," + hashHmacTripleDes + "\n" + "HMACSHA1," + createHmacSha1 + "," + hashHmacSha1 + "\n" + "HMACSHA256," + createHmacSha256 + "," + hashHmacSha256 + "\n" + "HMACSHA384," + createHmacSha384 + "," + hashHmacSha384 + "\n" + "HMACSHA512," + createHmacSha512 + "," + hashHmacSha512 + "\n" + "HMACMD5 (keyed)," + createHmacMd5Keyed + "," + hashHmacMd5Keyed + "\n" + "HMACRIPEMD160 (keyed)," + createHmacRipeMd160Keyed + "," + hashHmacRipeMd160Keyed + "\n" + "MACTripleDes (keyed)," + createHmacTripleDesKeyed + "," + hashHmacTripleDesKeyed + "\n" + "HMACSHA1 (keyed)," + createHmacSha1Keyed + "," + hashHmacSha1Keyed + "\n" + "HMACSHA1 (keyed+managed)," + createHmacSha1KeyedManaged + "," + hashHmacSha1KeyedManaged + "\n" + "HMACSHA256 (keyed)," + createHmacSha256Keyed + "," + hashHmacSha256Keyed + "\n" + "HMACSHA384 (keyed)," + createHmacSha384Keyed + "," + hashHmacSha384Keyed + "\n" + "HMACSHA512 (keyed)," + createHmacSha512Keyed + "," + hashHmacSha512Keyed + "\n" ; } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } }
If you want to try out the test yourself, simply paste the above code into a TestScript.cs
file in your Unity project’s Assets
directory and attach it to the main camera game object in a new, empty project. Then build in non-development mode for 64-bit processors and run it windowed at 640×480 with fastest graphics. I ran it that way on this machine:
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.10.5
- Unity 5.1.2f1, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And here are the results I got:
Hash Algorithm | 1000x Create Time | 1MB Hash Time |
---|---|---|
MD5 | 14 | 6 |
MD5CryptoServiceProvider | 4 | 4 |
RIPEMD160Managed | 10 | 27 |
RIPEMD160 | 8 | 25 |
SHA1Managed | 1 | 9 |
SHA1CryptoServiceProvider | 2 | 8 |
SHA256Managed | 1 | 16 |
SHA256 | 6 | 15 |
SHA384Managed | 0 | 12 |
SHA384 | 5 | 12 |
SHA512Managed | 0 | 16 |
SHA512 | 6 | 19 |
HMACMD5 | 13 | 17 |
HMACRIPEMD160 | 14 | 33 |
MACTripleDes | 21 | 203 |
HMACSHA1 | 13 | 16 |
HMACSHA256 | 12 | 21 |
HMACSHA384 | 12 | 16 |
HMACSHA512 | 12 | 21 |
HMACMD5 (keyed) | 13 | 16 |
HMACRIPEMD160 (keyed) | 16 | 38 |
MACTripleDes (keyed) | 14 | 201 |
HMACSHA1 (keyed) | 11 | 17 |
HMACSHA1 (keyed+managed) | 10 | 16 |
HMACSHA256 (keyed) | 8 | 21 |
HMACSHA384 (keyed) | 8 | 16 |
HMACSHA512 (keyed) | 8 | 21 |
There’s a lot of variability in the results, so I’ve split them up into three graphs to compare similar tests. All in its own test is the MACTripleDES
class which is horrendously slow compared to every other algorithm. Don’t use it unless you really have to.
Elsewhere I’ve split the keyed/HMAC algorithms from the non-keyed algorithms. The non-keyed ones tend to be faster, especially in creation time. Unless you need to use a keyed algorithm for security reasons, stick with the non-keyed version for performance.
One standout across both keyed and non-keyed algorithms is RIPEMD160, which is quite a lot slower at hashing than the rest of the algorithms except TripleDES. Again, unless you need it specifically it’s better to look elsewhere.
That leaves the rest of the algorithms as close competitors. Even so, there are up to 3x differences between them. The classic MD5 is the slowest to create, but hashes the quickest. Cache any instances you create if you’re going to hash a lot of byte arrays!
The SHA algorithms are roughly in order of their complexity. SHA-1 is fastest, then the SHA-2 family: SHA-256, SHA-384, SHA-512. For some reason though, SHA-384 clocks in a lot faster than either SHA-256 or SHA-512.
Finally, there are various classes for any given algorithm. Many have “managed” and regular versions and sometimes there is a “CryptoServiceProvider” version. The “managed” version is usually slower, except in the case of SHA-512 and HMAC SHA-1. The “CryptoServiceProvider” classes for MD5 and SHA-1 are also quicker, so you should prefer those when using those algorithms.
Exactly which algorithm you choose will be up to your unique requirements of creation speed, hashing speed, digest (i.e. output) size, and cryptographic strength. However, the above performance numbers should help if that’s a consideration in your choice.
Let me know in the comments if you’ve got any thoughts or experience hashing in Unity.
#1 by henke37 on September 7th, 2015 ·
You paint a misrepresentative picture by only including cryptographically secure hash functions. You should include other hashes like flat out summing the octets, various kinds of CRC and so on. Not secure, but quite a bit faster I’d imagine.
#2 by jackson on September 7th, 2015 ·
If I were to include non-cryptographic hash functions, I’d be comparing apples to oranges. I think that would paint a less fair picture. That said, you do have a point that the picture I paint could be more complete by including more hash functions. Unfortunately, the CRC family of algorithms isn’t available in Unity so you’d need to implement them yourself. For the purposes of this article, that was beyond the scope.
However, I just implemented a simple summation of the bytes as you suggested and it is indeed much faster: about one millisecond in the same test environment. That does compare favorably to
MD5CryptoServiceProvider
, the second-fastest algorithm, which clocked in at four milliseconds.#3 by Etherlord on September 7th, 2015 ·
Why would you, instead of comparing two arrays in the simplest possible way, run these arrays through a series of complex cryptographical computations in order to come up with hashes to compare them?
If you modify arrays rarely and compare them often, then Henke37 seems to have a point – or you could just just make your own algorithm based on the characteristics of the arrays. I’m not even sure if self-made algorithms won’t yield better results (both execution time and probability to give same hash for different arrays).
#4 by jackson on September 7th, 2015 ·
For the speed of a simple summation algorithm, see my response to Henke37 above. As for the purpose of hash algorithms in the first place, they have many uses. One is speed of comparison. It’s true that you do need to compute the hash by looping over the whole array, but the difference is who does the looping and when that looping occurs.
Say a client wants to download assets from server and those assets may be updated over time. The first time the client downloads the asset, hashes it, and stores both the hash and the asset on the local file system. When a new version of the asset is published to the server, the asset is hashed with the same algorithm. To check for updates, the client sends their hash of the asset to the server and the server compares against its own hash. By sending only ~20 bytes of data over the network and comparing only ~20 of data on the server, the server knows not to transmit much larger assets to the client, saving tons of bandwidth and time. A similar strategy is employed by the
WWW
class’ LoadFromCacheOrDownload and HTTP Etags.Wikipedia actually has a pretty good list of uses for hash functions. Others include obfuscation of strings and validating data integrity.
#5 by Etherlord on September 9th, 2015 ·
I agree that hashes might be useful, but the article is about cryptographic hash functions, where much simpler (and faster) algorithms could be used for some mentioned things. I agree that if there are no simple hashing utilities, then a cryptographic one could be used, and the test is helpful to show that MD5 is the best for such scenario.
However I don’t agree with another example you supplied: if you store a hash, then you could as well store a version number for the array, that would be both smaller and less wasteful on resources (unless you value memory so much more than CPU, that you actually don’t store hashes on the server and instead recompute them every time they’re needed). I can imagine some emergency situation where you need to compare some stuff, and you didn’t predict such need beforehand, so you don’t have a version number, but you can compute a hash. However I also imagine people using your suggestion to create very inefficient designs, as well as choosing the fastest algorithms for cryptography, while cryptographic hash functions are often slow by design (and you obviously know the issue judging by some parts of your article, but readers may not).
So I just leave it here for a warning.
#6 by jackson on September 9th, 2015 ·
These are fine points and a good demonstration of how better control over your asset pipeline can lead to efficiencies. If you have total control over the asset production and consumption within a product and the assets are versioned, a very small version number can be cheaply used as metadata instead of a hash.
In my example above I introduced an inefficiency. I had the client hash the asset once it was downloaded, but that’s not necessary at all. The asset pipeline can compute hashes at build time and the server can serve up the hash along with a request for the asset. That’s actually a much more typical approach than spending a lot of client CPU cycles hashing assets. In that case the hashing algorithm’s performance has no bearing on the client and therefore it’s better to choose a cryptographic one to better avoid collisions.
Really, as always, it comes down to the individual app’s requirements. Version numbers can reduce the digest size to one or two bytes but are less flexible for things like A/B tests and non-linear asset rollouts. Hashes, on the other hand, are more flexible but require more work to compute at build time and have larger digest sizes (usually 16-64 bytes). The choice may be clear within the context of a single app or type of app, but I don’t think there is one clear choice for all use cases.