|C# Bit Bashing - The BitConverter|
|Written by Mike James|
|Tuesday, 20 May 2014|
Page 1 of 2
Is C# a high-level or a low-level language? It doesn't really matter - all languages are low-level when you are thinking in terms of bits, and sometimes you just can't avoid thinking in bits.
In a high-level language there should be no need to provide low-level facilities.
Well not really.
There are all sorts of reasons why any language needs to get down to the level of the actual bits that make up any real program. Often it is forced on the programmer by the need to use legacy data or work with raw data generated by another program or hardware device.
In principle you can avoid it by introducing abstractions such as sets and so on but at the end of the day it is usually simpler and more direct to work with the data as it is actually represented inside the machine. Sad, regrettable but mostly true.
Sooner or later you are going to have to solve a problem that comes down to working with the bit patterns that represent a variable’s value.
So how should a high-level language handle such low-level work?
In the case of C# one important consideration is the need to stay in the realm of managed code. Of course it is possible to descend to almost any level if you are happy with “unsafe” and P/Invoke, but ideally C# should provide bit operations that are safe because they are part of managed code.
What is interesting about C# is that you can conclude that it isn’t very good at low-level bit manipulation until you discover the BitConverter class and a few other reasonably well hidden facilities.
The basic bit manipulation operations that are provided can be confusing because of the availability of similar operators specialised to Boolean data types.
The bitwise operations are:
These all work on integer data types and return the corresponding bitwise operation.
computes Not(a) and b.
It is also worth introducing the “x” format specifier as used in 0xF0 to enter a hexadecimal format number.
They don’t work on floating point types or custom types unless you go to the trouble of overloading the operators.
They do also work on Booleans, however, and return just True or False according to the usual rules of the truth table.
There are also three logical operators that only work with Boolean:
These only work with Boolean variables and return the same true or false results as the bitwise operators.
The And && and Or || operators are special because they are “lazy” evaluation operators.
If you write:
then w and v will have the same value. However, in the case of && the y variable will not be used at all because the fact that x is false means that the entire result has to be false no matter what values other variables have. In general: x&&y only evaluates y if x is true and x||y only evaluates y if x is false.
This is in general not a question of efficiency but allows for the possibility that evaluating y when the condition was already determined by x would cause an error of some sort.
throws an exception because it actually attempts to perform the illegal divide by zero whereas:
doesn’t because the lazy evaluation avoids trying to divide when a is zero.
In addition to the bitwise logical operators C# also has a left and right shift operator. For example:
shifts the contents of a eight bits to the right and
shifts the contents eight bits to the left.
You can consider a left shift to be an integer multiply by two and a right shift to be a multiply by two.
If the data type being shifted is a signed 32 or 64-bit type then an arithmetic right shift is performed – i.e. new high order bits are set to the sign bit.
If the data type is an unsigned 32 or 64-bit type then a logical right shift is performed i.e a zero bit is shifted into the high location.
If you want to perform logical shift on a signed type you can simply use an explicit cast.
performs an arithmetic shift right but:
performs a logical shift right, i.e. it shifts in zeros from the right.
All of the logical operators and the shifts work with 32 or 64-bit data types.
In the case of the simple logical operations 16 and 8-bit data is automatically converted to 32-bit types and you need to use an explicit cast to recover the original data type.
Things are just as simple with shifts. For example, consider:
You will discover that the answer is 0xFF00.
There is also a problem with signed types.
If you attempt to assign a hex value that represents a negative number, for example:
you’ll find it doesn’t work. If you want to force it to work then use:
where the variable declaration has now to be separate otherwise it would only be defined within the scope of the “unchecked”.
If you don’t want this, use unsigned types as in:
You could argue that using unsigned types is the best way to represent bit patterns but it is often necessary to perform bit manipulation on other data types simply because this is the object of the exercise.
You can use combinations of shifts and logical operators to create other types of shift including bit rotates.
For example, a rotate left, routine is fairly easy to create:
This works by first creating a mask with the top n bits set to one by using an arithmetic shift.
Next we use the mask to extract the top n bits which we shift right to become the bottom n bits. Finally the shift left by n is performed and what were the top n bits are stored as the bottom n bits of the result. Of course this could be written in a more compressed form by combining expressions but the important question of how efficient this is could only be answered by benchmarking against alternative methods such as performing a repeated shift and high to low bit transfer.
|Last Updated ( Tuesday, 20 May 2014 )|