0

I was just trying to optimize some code when I came across this code in the .NET BinaryReader class:

[CLSCompliant(false)]
[__DynamicallyInvokable]
public virtual ulong ReadUInt64()
{
  this.FillBuffer(8);
  return (ulong) (uint) ((int) this.m_buffer[4] | (int) this.m_buffer[5] << 8 | (int) this.m_buffer[6] << 16 | (int) this.m_buffer[7] << 24) << 32 | (ulong) (uint) ((int) this.m_buffer[0] | (int) this.m_buffer[1] << 8 | (int) this.m_buffer[2] << 16 | (int) this.m_buffer[3] << 24);
}

I would like to know why the ulong is shifted in two parts and then combined rather than just shifting in 0,8,16,..,56.

I would also like an explanation of the multiple castings used here.

EDIT : When I type this out in VS2013 with ReSharper installed, it displays as this:

enter image description here

As you can see, the second ulong is darkened out, meaning it is not even used. It appears a pair of brackets are missing. Is this right?

Hele
  • 1,558
  • 4
  • 23
  • 39
  • What is the framework version? Checking the reference source, and by decompiling 4.5.1 BinaryReader, the source code is different in ReadUInt64() – Oguz Ozgul Dec 02 '15 at 08:58
  • This is what is says in the comment at the top of the file : v4.0.30319 – Hele Dec 02 '15 at 09:04

1 Answers1

3

Firstly, let's rewrite this in a more readable form:

(ulong)(uint)(
    (int) this.m_buffer[4]       | 
    (int) this.m_buffer[5] << 8  |
    (int) this.m_buffer[6] << 16 |
    (int) this.m_buffer[7] << 24)
<< 32 | 
(ulong)(uint)(
    (int) this.m_buffer[0]       |
    (int) this.m_buffer[1] << 8  | 
    (int) this.m_buffer[2] << 16 | 
    (int) this.m_buffer[3] << 24)

Now it's clear what this is doing.

It is combining the low 32 and high 32 bits into two separate uint values (by shifting the bytes after casting them to int), then casting the two resulting int values to ulong, shifting the high 32 bits by 32 and finally ORing them together to get the result.

The reason it does this is because it is faster to shift 32 bit values than it is to shift 64 bit values. If the code cast all the bytes to ulong and then shifted them, it would be slower.

Also note that the order in which it is combining the bytes is for "little endian" format.

The only apparently weird thing is the first cast (ulong)(uint), which could just be (ulong). If you remove all the unnecessary casts, it looks like this:

(ulong)(
    buffer[4]       |
    buffer[5] << 8  |
    buffer[6] << 16 |
    buffer[7] << 24)
<< 32 |
(uint)(
    buffer[0]       |
    buffer[1] << 8  |
    buffer[2] << 16 |
    buffer[3] << 24);

Now you might wonder why you can't just cast to ulong instead of uint.

The answer to this is as follows. Suppose the code was like this:

(ulong)(
    buffer[4]       |
    buffer[5] << 8  |
    buffer[6] << 16 |
    buffer[7] << 24)
<< 32 |
(ulong)(         // This seems more natural...
    buffer[0]       |
    buffer[1] << 8  |
    buffer[2] << 16 |
    buffer[3] << 24);

The first part of the expression is shifted using ulong.operator<<() as a result of the cast to (ulong). This returns a (ulong).

However the second part of the expression is NOT shifted, and when ORing it with the first expression it prompts a compiler warning:

warning CS0675: Bitwise-or operator used on a sign-extended operand; consider casting to a smaller unsigned type first

Eric Lippert has a blog about this warning which you should read.

Anyway, because it looks weird when casting to (ulong) in the first part and (uint) in the second part, I'm fairly sure they put in the full explicit casts to prevent confusion on the part of the reader.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276