2

I have created 3 different methods for converting a byte[] into a hex string using the following format: { 0xx2, ... } (Working Demo)

using System;
using System.Diagnostics;
using System.Text;

public class Program
{
    public delegate string ParseMethod(byte[] Msg);
    public class Parser 
    {
        public string Name { get; private set;}
        public ParseMethod Handler { get; private set; }

        public Parser(string name, ParseMethod method)
        {
            Name = name;
            Handler = method;
        }
    }

    public static void Main()
    {
        Parser HexA = new Parser("ToHexA", ToHexA);
        Parser HexB = new Parser("ToHexB", ToHexB);
        Parser HexC = new Parser("ToHexC", ToHexC);

        TestCorrectness(HexA);
        TestCorrectness(HexB);
        TestCorrectness(HexC);

        Console.WriteLine("Small Message Performance:");
        TestPerformance(HexA, MsgSize: 10, Iterations: 1000000);
        TestPerformance(HexB, MsgSize: 10, Iterations: 1000000);
        TestPerformance(HexC, MsgSize: 10, Iterations: 1000000);
        Console.WriteLine();

        Console.WriteLine("Large Message Performance:");
        TestPerformance(HexA, MsgSize: 500, Iterations: 1000000);
        TestPerformance(HexB, MsgSize: 500, Iterations: 1000000);
        TestPerformance(HexC, MsgSize: 500, Iterations: 1000000);
        Console.WriteLine();
    }

    private static void TestCorrectness(Parser parser)
    {
        Console.WriteLine("Testing Correctness of \"{0}(byte[] Msg)\"", parser.Name);
        Console.WriteLine("\"{0}\"", parser.Handler(new byte[]{}));
        Console.WriteLine("\"{0}\"", parser.Handler(new byte[]{ 97 }));
        Console.WriteLine("\"{0}\"", parser.Handler(new byte[]{ 97, 98, 99, 0, 100 }));
        Console.WriteLine();
    }

    private static void TestPerformance(Parser parser, int MsgSize, int Iterations)
    {
        Stopwatch sw = new Stopwatch();
        sw.Reset();

        byte[] Msg = new byte[MsgSize];

        sw.Start();
        for (uint i = 0; i < Iterations; ++i)
        {
            parser.Handler(Msg);
        }
        sw.Stop();

        Console.WriteLine("Performance for \"{0}\", {1}", parser.Name, sw.Elapsed);
    }

    private static string ToHexA(byte[] buffer)
    {
        return
            (
                "{ 0x" +
                    BitConverter.ToString(buffer).ToLower()
                        .Replace("-", ", 0x") +
                " }"
            )
            .Replace(" 0x }", "}");
    }

    private static string ToHexB(byte[] buffer)
    {
        if (buffer.Length == 0) { return "{}"; }

        const string Preamble = "{ 0x";
        const string Delimiter = ", 0x";
        const string Epilogue = " }";

        string Msg = Preamble + buffer[0].ToString("x2");
        for (int i = 1; i < buffer.Length; ++i)
        {
            Msg += Delimiter + buffer[i].ToString("x2");
        }

        return Msg += Epilogue;
    }

    private static string ToHexC(byte[] buffer)
    {
        if (buffer.Length == 0) { return "{}"; }

        const string Preamble = "{ 0x";
        const string Delimiter = ", 0x";
        const string Epilogue = " }";

        StringBuilder HexOut = new StringBuilder(
            Preamble.Length +
            (Delimiter.Length * (buffer.Length - 1)) +
            (2 * buffer.Length) +
            Epilogue.Length
        );

        HexOut.Append(Preamble);
        HexOut.Append(buffer[0].ToString("x2"));
        for (int i = 1; i < buffer.Length; ++i)
        {
            HexOut.Append(Delimiter);
            HexOut.Append(buffer[i].ToString("x2"));
        }
        HexOut.Append(Epilogue);

        return HexOut.ToString();
    }
}

Running this code I get the following performance statistics

Small Message Performance:
Performance for "ToHexA", 00:00:01.3078387
Performance for "ToHexB", 00:00:01.6939201
Performance for "ToHexC", 00:00:01.2997903

Large Message Performance:
Performance for "ToHexA", 00:00:32.5230253
Performance for "ToHexB", 00:04:23.4798762
Performance for "ToHexC", 00:00:56.2404684

I find it surprising that ToHexA (relative to ToHexC) executes faster with a longer message but slower with a shorter message. As I understand it, Replace(), +, and ToLower() all have to perform a create/copy operation because strings are immutable.

At the same time, I suspected the overhead for StringBuilder might make it less suitable for shorter messages yet it executes faster than ToHexA in this situation.

The only thing I expected was the performance hit associated with ToHexB which is kind of an in between worst of both worlds scenario...

So, what is going on with these two methods and their performance characteristics?

Edit to make my intentions with this question more clear

I am not particularly interested in exact performance characteristics of two methods that are closely performant. I am aware this will be different on different devices, architectures, background processes, etc etc. and better testing methods exist to control variables and what not

My purpose in asking this question was to better understand why two noticeably dissimilar (in terms of performance) approaches stack up the way they do. The accepted answer explains the mechanics of ToString in a way that satisfies this question.

One constraint of this question the exact formatting of the output was specific and (we can only muse why, I think it's pretty standard) for some reason not one of the standard output formats in C#/.NET; it was this unique format that led to the question: stacking multiple replace and concatenation operations against StringBuilder

Assimilater
  • 944
  • 14
  • 33
  • 2
    Please do performance tests with a proper benchmarking suite instead of something naive like this. This is plain useless. – Jeroen Vannevel Jul 07 '17 at 20:55
  • @JeroenVannevel Sounds reasonable, can't help that I was naive though. Lots of accepted SO answers recommended this kind of approach to bench-marking though, with caveats that you shouldn't optimize... – Assimilater Jul 07 '17 at 20:57
  • 1
    I disagree with @JeroenVannevel, this is fine for a quick test. The only thing useless about your results is your tiny number of iterations. The difference in times can be chalked up to random system performance variations. Try do a million iterations instead. – SledgeHammer Jul 07 '17 at 20:57
  • @SledgeHammer Also a point I thought of. I just ran out of allotted memory on the demo site. I'll run a larger test on my personal machine real quick and update – Assimilater Jul 07 '17 at 20:59
  • Once you scale out, I personally would expect method A to be the fastest. Your other 2 methods are doing a lot of string manipulation and formatting where as BitConverter doesn't do any of that. It works on a character array internally and builds up the characters and finally builds a string at the end. – SledgeHammer Jul 07 '17 at 21:03
  • @SledgeHammer I ran the test for a million iterations, and updated (the big test is actually still running). However, it is as you say, method A still is faster (except for small sizes) – Assimilater Jul 07 '17 at 21:23
  • 1
    You might be interested in [BenchmarkDotNet](http://benchmarkdotnet.org/index.htm) for an easy to use benchmarking library. – Chris Dunaway Jul 07 '17 at 21:23
  • 1
    @SledgeHammer: Operating on a character array is basically the exact same thing that `StringBuilder` does, and as time tests of my answer show, `StringBuilder` append operations are no problem at all. – Ben Voigt Jul 07 '17 at 21:40
  • @BenVoigt getting rid of the ToString is an improvement, but you are still doing a lot of overhead (StringBuilder appends) where as BitConverter doesn't. It operates on a single character array that has been sized for perfection. StringBuilder is fast, but does a lot of manipulation of chunks. Your solution beat BitConverter? If so, I am surprised. – SledgeHammer Jul 07 '17 at 22:11
  • 1
    You might be interested in [this answer](https://stackoverflow.com/a/624379/80274) to a similar question with benchmarks of very many different ways to go from byte[] to hex and back. – Scott Chamberlain Jul 07 '17 at 22:24
  • @ChrisDunaway Thanks, looks like that will be useful in the future :) – Assimilater Jul 07 '17 at 23:47
  • @SledgeHammer as I understand it the primary purpose of `StringBuilder` is that append operations are done in-place (unless it has to resize, which ya that's a performance hit) – Assimilater Jul 07 '17 at 23:48
  • As for the down votes and the close votes...is it just because I didn't use a professional testing benchmark? I feel like the question is sound: what is it about approach c that is slower. Especially judging by the answer that was procured. Maybe my presentation could be improved? But as near as I can tell I followed every community guideline in making this question so.... – Assimilater Jul 07 '17 at 23:51
  • In particular one community guideline is to not request info about available tools as it often results in a list. As such I kept it tool agnostic (to avoid the question leading to your tool sucks) figuring a simple test is sufficient. I'm not trying to be over performant as much as understand the features of the language a little more deeply. – Assimilater Jul 07 '17 at 23:55
  • @ScottChamberlain appreciate the link, don't mind it being attached to this post though I saw it in my readings. I wasn't so much concerned about the actual performance characteristics other than there was a noticeable difference between the two methods and I couldn't explain it easily. – Assimilater Jul 08 '17 at 00:16
  • @Assimilater, StringBuilder is optimized for building strings (duh :) )... but it has "considerable" overhead. When you append an int, first of all, it will do a ToString on it (BitConverter does not). Then it goes through a bunch of code to see if it will fit in the current chunk (and if not, create a new one), etc. It definitely does not do it "in place" in the same way that BitConverter does. – SledgeHammer Jul 08 '17 at 00:32
  • 1
    @Assimilater, download a free tool called ILSpy and compare the code between StringBuilder.Append and what BitConverter does. You'll see that BitConverter is just a few simple math ops and array manipulation where as StringBuilder is much, much more. – SledgeHammer Jul 08 '17 at 00:33
  • @SledgeHammer Ah, I can see where you're coming from now. But if you look, I append strings, and the answer appends chars, so no ToString (other than the explicit `byte.ToString`, duh lol). As for checking dimensions that makes sense. I don't think it's enough to justify out performing a `ToLower`, 2 `Replace` calls and 2 concatenations, however (where as the accepted answer does explain things) – Assimilater Jul 08 '17 at 00:35
  • 1
    @Assimilater... well, I wouldn't say your BitConverter implementation is optimal :). Copy the method using ILSpy (its very short) and modify it to return lower case chars (if it doesn't already) and that gets rid of ToLower(). You can also specialize it to set the { 0x } portions... that'd get rid of the string concats. And once you specialize it there is no need for String.Replaces either :). That'd be your fastest solution by far. – SledgeHammer Jul 08 '17 at 00:44
  • @SledgeHammer sounds worthy of an answer to me (I'd upvote it) - I mean it's kind of not exactly the question but it's related enough – Assimilater Jul 08 '17 at 00:46
  • 1
    @SledgeHammer: While it's true that `StringBuilder` has "a bunch of code" for extending the internal buffer when needed, when it isn't growing that code doesn't execute and the actual cost is just a bounds check (which is also part of `char[]` manipulation, unless you use pointers and an `unsafe` block). The question already had proper preallocation, so "a few simple math operations and array manipulation" is precisely what `StringBuilder` will do. Not all that it is capable of, but all that this case uses. – Ben Voigt Jul 08 '17 at 00:56
  • @BenVoigt, actually, you are appending strings in 3 places (one of which is in the loop). String appends will internally do a string copy through Buffer.Memmove. I agree that appending a char is cheap if the buffer is big enough. No string copies, etc. with the custom BitConverter solution I described above. Also, a further micro-optimization in the BitConverter code is they cache the byte rather then accessing it through the array twice. – SledgeHammer Jul 08 '17 at 01:11
  • 1
    @SledgeHammer: `memmove` is extremely cheap for filling in a character array. `BitConverter` doesn't use it because the separator is only one character.... but the later string `Replace` will, and the customization of the `BitConverter` code that was suggested would either `memmove` or something even slower (e.g. assigning each character individually) – Ben Voigt Jul 08 '17 at 01:19
  • @BenVoigt -- There is no string Replace, ToLower or any string operation at all in the customized BitConverter method I suggested above. All that is eliminated. I didn't say copy the method lol, I said copy it and customize it to format the string as you want it. I do something similar for an ultra high performance date time formatting method and it is substantially faster. If you think your solution is faster then assigning a few chars to an array, I dunno what else to tell ya lol... I can assure you its not. You do a lot of "expensive" stuff like method calls, string appends, etc. – SledgeHammer Jul 08 '17 at 02:33
  • @SledgeHammer: The customized version has to append a multicharacter separator (`" ,0x"`) to the character array somehow, many times -- that is where I suppose the customized BitConverter will be using a `memmove`. One element at a time is not the fastest way to fill in the character array! You do know that ILSpy and dotPeek show you intermediate language, not the actual machine instructions, correct? Where the MSIL has a "method call", the JIT compiler may be performing inlining. `Append(char)` surely will get inlined, `Append(string)` is likely to as well. – Ben Voigt Jul 08 '17 at 02:41
  • @BenVoigt -- YAWN... a) your code doesn't even compile since char[] is not equivalent to byte[] b) I fixed it for you and did a benchmark. I modified nothing else other then the digits declaration. I then benchmarked it against ToHexA() "AS IS". Your method = 3s, HexA "AS IS" = 1s. Thanks for playing though. Better luck next time. And that was WITHOUT my suggested optimizations. – SledgeHammer Jul 08 '17 at 03:01
  • @SledgeHammer: I pasted the exact code in my answer into the question "Working Demo", made no change other than `ToHexD` -> `ToHexB` (so the demo would call it), and it ran fine and in half the time of `ToHexA`. So you clearly did not try my actual code, but some corrupted version of it. – Ben Voigt Jul 08 '17 at 03:03
  • @BenVoigt... I c&p'ed the code you pasted below... but for the sake of argument, I'll copy the code from where you want me too... HexB = 1.8s, HexC = 1.4s. Neither are as fast as the original HexA which is 1s (again WITHOUT optimizations). What's your next argument? That I don't know how to compile or run code? – SledgeHammer Jul 08 '17 at 03:09
  • 1
    Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148659/discussion-between-ben-voigt-and-sledgehammer). – Ben Voigt Jul 08 '17 at 03:10
  • @SledgeHammer: WITHOUT optimizations may in fact be a big part of your problem. The library code is already compiled with optimizations turned on, turning them off in my version is unfair in the extreme. Here's the comparison I see, with ToHexD beating ToHexA by a factor of over 3: http://rextester.com/PRBNVL54136 – Ben Voigt Jul 08 '17 at 03:14
  • @BenVoigt... I meant my suggested optimizations, not compiler optimizations, duh. I tested all HexA, HexC, HexD under the same conditions in Visual Studio 2015. – SledgeHammer Jul 08 '17 at 03:22
  • @BenVoigt, I did implement some of my suggestions and the time on HexA went down substantially to 203ms, soundly defeating your method. – SledgeHammer Jul 08 '17 at 03:23
  • @SledgeHammer: Absolute times mean nothing, they depend more on the computer you're running on than the code. Post the code so it can be compared on the same system as the other results. – Ben Voigt Jul 08 '17 at 03:25
  • 1
    @SledgeHammer: You'll also note that I never claimed that the code in my answer squeezed out every last drop of performance either -- quite the contrary, I explained where the next improvement could be made. My goal was to explain where the bottleneck was in the code in the question, and I gave improved code proving it. – Ben Voigt Jul 08 '17 at 03:30
  • @BenVoigt -- good lord dude. I ran them on the same machine. Same conditions. Original HexA = 1s, from your dotnetfiddle link, HexB = 1.8s, HexC = 1.4s. If I just remove the ToLower() (nothing else) on the original HexA function since that wouldn't be needed, it drops down to 800ms. Each of the .Replaces is about another 200ms each. Those wouldn't be needed either. – SledgeHammer Jul 08 '17 at 03:34
  • @BenVoigt... no, you've just spent the last 3 hours pointing out bottlenecks in the original code and produced code that is 50% slower then the original LOL... – SledgeHammer Jul 08 '17 at 03:35
  • @SledgeHammer: You must actually be doing something wrong in the compile settings, because I tested three machines (dotnetfiddle.net, rextester.com, and my Xeon box with VS2015 compiling optimized for x64) -- and on all 3, ToHexD takes less than half the time of ToHexA. Are you running my *exact* code from my answer, or the version where you changed `char[]` to `byte[]` plus some other things you called "fixes" that really made it worse? – Ben Voigt Jul 08 '17 at 03:39
  • This is what I got from my machine, increasing the number of runs because I'm not subject to some online limit, and dividing the time by the number of runs: Big Message Performance: Performance for "ToHexA", 0.023707428 Performance for "ToHexB", 0.21169363 Performance for "ToHexC", 0.056575678 Performance for "ToHexD", 0.008242704 Small Message Performance: Performance for "ToHexA", 0.00092702684 Performance for "ToHexB", 0.0015206616 Performance for "ToHexC", 0.00124264751 Performance for "ToHexD", 0.00029889859 – Ben Voigt Jul 08 '17 at 03:41
  • @SledgeHammer Here's the exact version I ran on my machine, let me know if you see anything unfair. http://rextester.com/EIVS60314 – Ben Voigt Jul 08 '17 at 03:43
  • @BenVoigt, I retook the code EXACTLY. Brand new project. For starters, you aren't even populating anything in the Msg array in. You're just new'ing it up with all 0's. I populated it with "0123456789abcdef" as bytes. HexA=1s, HexB=1.7s, HexC=1.4s. Same as before. Same PC, same compiler, same conditions, no background processes or anything that might throw it off. When I used a 1000 empty byte array, I couldn't even run 50K iterations on B & C, they were taking too long. A ran in 2.4s. – SledgeHammer Jul 08 '17 at 04:09
  • @BenVoigt -- sorry man, you can not possibly believe that direct array manipulation will be slower then StringBuilder. StringBuilder is "slow" and "expensive". Even MS tells you that if you just want to do a + b + c (a few simple concats) not to use it. They say to only use it if each string is dozens of concats -- in this particular case.. direct array access is much, much faster. – SledgeHammer Jul 08 '17 at 04:12
  • @SledgeHammer: Good catch on the all-zeros input (I didn't change it from the code in the question). But even with `rng.NextBytes(Msg);` right after the `new[]`, I still get 3x faster results (lower times) from `ToHexD` than `ToHexA`. And `StringBuilder` isn't slow or expensive, it is comparable to C# array access (since C# adds overhead to array access, such as bounds checking). Direct memory access will be faster than either one, of course. – Ben Voigt Jul 08 '17 at 04:17
  • Code with non-zero random data: http://rextester.com/LDAH5585 results: Big Message Performance: Performance for "ToHexA", 0.026898009 Performance for "ToHexB", 0.24803454 Performance for "ToHexC", 0.066089383 Performance for "ToHexD", 0.009640475 Small Message Performance: Performance for "ToHexA", 0.00088271005 Performance for "ToHexB", 0.0017626445 Performance for "ToHexC", 0.00137349571 Performance for "ToHexD", 0.00026311122 – Ben Voigt Jul 08 '17 at 04:19
  • @BenVoigt -- see my answer below. This is a highly tuned function that is 2x as fast as HexD. – SledgeHammer Jul 08 '17 at 05:10
  • @SledgeHammer your answer in my tests performs a little under 2x as fast for me. However, Ben's solution is faster than HexA and HexC. I think the confusion is you were maybe using my original StringBuilder code which still had `ToString`? I'm out of guesses after that. – Assimilater Jul 08 '17 at 06:04
  • To restate: my objective was in learning what it was about HexC that made it slower than HexA; which is primarily slow because of what I did outside of `BitConverter`, I acknowledge...that wasn't the point ;). That question has been answered :) – Assimilater Jul 08 '17 at 06:05
  • 1
    @SledgeHammer no need to download ILTool, you can get the source for both online from Microsoft. [StringBuilder](http://referencesource.microsoft.com/#mscorlib/system/text/stringbuilder.cs,900), [BitConverter](http://referencesource.microsoft.com/#mscorlib/system/bitconverter.cs,8640d8adfffb155b) – Scott Chamberlain Jul 09 '17 at 20:56

2 Answers2

2

Your approach C is going to be the fastest approach to concatenation, however you're still generating garbage with each call to ToString("x2").

In addition to that, the ToString overloads that take a format string are miserably slow for something like this, because they first have to process the format string before they can do actual work, and this format string processing gets repeated many many times. Too bad there isn't a Converter<byte, string> GetToString(string format) that processes the format string once and returns a stateful conversion object.

In any case, byte.ToString() is very easy to avoid here.

static readonly char[] digits = "0123456789abcdef".ToCharArray();
private static string ToHexD(byte[] buffer)
{
    if (buffer.Length == 0) { return "{}"; }

    const string Preamble = "{ 0x";
    const string Delimiter = ", 0x";
    const string Epilogue = " }";

    int expectedLength = 
        Preamble.Length +
        (Delimiter.Length * (buffer.Length - 1)) +
        (2 * buffer.Length) +
        Epilogue.Length;
    StringBuilder HexOut = new StringBuilder(expectedLength);

    HexOut.Append(Preamble);
    HexOut.Append(digits[buffer[0] >> 4]).Append(digits[buffer[0] & 0x0F]);
    for (int i = 1; i < buffer.Length; ++i)
    {
        HexOut.Append(Delimiter);
        HexOut.Append(digits[buffer[i] >> 4]).Append(digits[buffer[i] & 0x0F]);
    }
    HexOut.Append(Epilogue);

    return HexOut.ToString();
}

Two calls to Append(char) should also be faster than one to Append(string), though this is much less important than saving the time on format strings.

As expected, this beats the BitConverter-then-Replace() approach handily at all sizes (by just about a factor of two).

For a tiny bit of further increase in speed, the calculations buffer[i] >> 4 and buffer[i] & 0x0f can both be avoided by preparing lead-digit and tail-digit lookup tables of length 256 each. But bitwise operations are incredibly fast; on many microcontrollers lacking a large L1 data cache, increasing the size of the lookup tables would cost more performance than the bitwise ops.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
0

As discussed, here is a highly tuned function that is specialized for this requested output. Please forgive the "magic values" :), I threw this together as a POC :), but it is extremely fast which was the point. It avoids all string operations, StringBuilder, calling additional methods, etc.

private unsafe static string ToHexA2(byte[] buffer)
{
    int length = buffer.Length;
    int num = (length * 6) + 2;
    char[] array = new char[num];
    int num2 = 0;

    fixed (char* ptr = array)
    {
        *(long*)ptr = 0x0020007B;

        for (int i = 2; i < num; i += 6)
        {
            byte b = buffer[num2++];
            *(long*)(ptr + i) = 0x00780030;
            *(long*)(ptr + i + 2) = digits[b >> 4] | (digits[b & 0x0F] << 16);
            *(long*)(ptr + i + 4) = 0x0020002C;
        }

        *(long*)(ptr + num - 2) = 0x007D0020;
    }

    return new string(array, 0, array.Length);
}
SledgeHammer
  • 7,338
  • 6
  • 41
  • 86
  • +1 as I think this is relevant and helpful; I certainly didn't intend for this to turn into a contest, though ;) – Assimilater Jul 08 '17 at 06:06
  • You clearly copied this from some C++ code (you should give the source), without noticing that C# `long` is not the same size as C++ `long`. In C#, you should be using `int` or `System.Int32`, because right now you have a four byte buffer overflow. – Ben Voigt Jul 08 '17 at 15:09
  • @BenVoigt Wrong again Ben. The magic values are just the braces and spaces and the 0x encoded as longs since we are talking unicode. The main "logic" is copied from BitConverter.ToString() as I described... oh... like a million times. Where do you see a 4 byte overflow? – SledgeHammer Jul 08 '17 at 15:28
  • @BenVoigt, oh... and the pointer stuff was lifted from Buffer.MemMove without all the lazy overhead your implementation had by using StringBuilder. That's why its 2x as fast. You're just 0 for a million on this thread, aren't you? – SledgeHammer Jul 08 '17 at 15:30
  • You have a four byte overflow because the last line inside your `fixed` block writes 8 bytes to a location 4 bytes before the end of the array. I'm fully aware of what the "magic" values are, my comments about `memmove` writing more than one element at a time were intended to lead you toward this solution (except that you mucked it up by using 64-bit writes for 32 bits worth of data) – Ben Voigt Jul 08 '17 at 15:33
  • BTW it will be even faster once you start using 32-bit transfers, because they'll be properly aligned. Half of your current transfers are unaligned, causing a performance hit. – Ben Voigt Jul 08 '17 at 15:34
  • @BenVoigt -- YAWN... leading me to a solution LOL... You're like Trump. You just can't admit you're wrong and your solution is slow. The custom char array solution was *slightly* faster then your fastest solution. I went further with the multi-byte copy to test it out... – SledgeHammer Jul 08 '17 at 15:51
  • @BenVoigt -- Wrong, YET AGAIN Ben. VS2015 are set to prefer 32 bit by default. For the sake of argument, I set it to 64-bit and changed it to int's and its only like 10ms faster. Still waaaayyy faster then anything you wrote. – SledgeHammer Jul 08 '17 at 15:52
  • @SledgeHammer: In C#, `long` is an alias for `System.Int64`. Always. It doesn't depend on the architecture. You really should make sure you know what you're talking about before you start calling others wrong. – Ben Voigt Jul 08 '17 at 15:53