There is no myth. Let's talk about decimal's "blind eye" and decimal's blind eye.

0x00 Preface

In the last article "compromise and trade-offs, deconstruct decimal operations in C #", many of my friends talked about the decimal type in C. In fact, the idea of the previous article was mainly to talk about how binary Computers handle decimal places. What I was most exposed to was the high-level language C # running in a hosted environment #, C # is used as an example. On the one hand, it illustrates the nature of computer processing decimal places and reminds you to pay more attention to the nature rather than the representation of advanced languages. Of course, what I mentioned in that article is binary floating point double and float (that is, System. Double and System. Single. In this article, double and float are used to refer to these two types respectively ). However, I think it is necessary to write an article to talk about the decimal type, which is a unified reply to the friends who mentioned decimal in the message.

0x01 start with 0.1 and binary floating point number

Some friends tell me in private that in the previous article, if we simply say that 0.1 in decimal format cannot be accurately expressed in binary format, although theoretically, however, after all, we didn't get an intuitive impression through direct observation. So before officially introducing decimal, let's take a look at why decimal 0.1 cannot be accurately expressed by binary floating point numbers.

As in decimal, 1/3 cannot be accurately expressed. If we want to convert 1/3 to decimal form, it is:

1/3 = 0. 3333333... (3 cycles)

Similarly, decimal 0.1 cannot be accurately expressed by binary decimal places. If we want to convert decimal 0.1 to binary decimal places, it is:

0.1 = 0. 00011001100... (1100 loop)

We can see that if you want to convert decimal 0.1 to binary decimal places, the 1100 loop will occur. Therefore, based on the IEEE 754 standard that I mentioned in the previous article and the last example in the previous article, we first set 0. 00011001100 .... perform logical shift so that the first place on the left of the decimal point is 1. The result is 1. 10011001100.... A total of four digits are moved, so the corresponding index is-4. Therefore, the result of float binary floating point number indicating decimal 0.1 is as follows:

Sign bit: 0 (indicating positive number)

Exponential part: 01111011 (01111011 is converted to decimal type 123, and the result is-4 because-127 is subtracted)

Tail part: 10011001100110011001101 (that is, after the shift, remove 1 to the left of the decimal point, leave the decimal part, and retain 23 digits)

So what is the actual number of float binary floating-point numbers used to "represent" decimal places of 0.1? What is the difference between it and 0.1? The following is a conversion:

Index part: 2 ^ (-4) = 1/16

Tail part: 1 + 1/2 + 1/16 + 1/32 + 1/256 + 1/512 + 1/4096 + 1/8192 + 1/65536 + 1/131072 + 1/1048576 + 1/2097152 + 1/8388608 = 1.60000002384185791015625 (1 on the left of the decimal point is omitted when it is converted to float., here we need to add it back again)

Then, the actual decimal number after conversion is: 1.60000002384185791015625*1/16 = 0.100000001490116119384765625

So we can see that the binary floating point does not accurately represent the decimal number of 0.1. It uses 0.100000001490116119384765625 instead of 0.1.

This is the direct use of binary to represent decimal places, which may produce errors.

0x02 decimal obstacle vision

However, many friends mentioned that using decimal to avoid errors in the above text. Indeed, using decimal is a very safe measure. However, Why can a computer use the decimal type to calculate the decimal number perfectly? Does a computer change its most fundamental binary operation when it involves decimal operations?

Of course not.

As I mentioned in the previous article, "We all know that computers use 0 and 1, that is, binary. It is very easy to use binary to represent integers ". Is it possible to indirectly use Integers to represent decimal places? Because binary represents a perfect decimal integer.

The answer is true. However, before we discuss the decimal details, I think it is necessary to briefly introduce decimal.

Here, decimal refers to the System in C. decimal, although only two floating point float and double (binary floating point number) are mentioned in the C # language specification, if we understand the definition of floating point number, decimal is obviously also a floating point number-but its base number is 10, so it is a decimal floating point number.

Decimal Structure

Similarly, the composition of decimal, float, and double is very similar: Symbol bit, index part, and tail part.

Of course, decimal has more bits, which reaches 128 bits in total. In other words, it has 16 more bytes. If we divide the 16 bytes into four parts, we can look at its composition structure.

The following uses m to represent the ending part, e to represent the exponent part, and s to represent the symbol bit:

1 ~ Byte 4: mmmm

5 ~ Byte 8: mmmm

9 ~ Byte 12: mmmm

13 ~ 16 byte: 0000 0000 0000 0000 000e eeee 0000 000 s

From its composition structure, we can see that the ending part of decimal has 96 bits (12 bytes), while the exponent part is only 5 bits, and the symbol bit is naturally only 1 bits.

Tail number of decimal

Now let's pull the idea back to the beginning of this section. If we use Integers to represent decimals, decimal can more accurately represent a decimal. Here we can see that the decimal*The ending part is actually an integer.*The range indicated by the ending number is also very clear: 0 ~ 2 ^ 96-1. Convert to decimal to 0 ~ 79228162514264337593543950335, a 29-digit number (of course, the maximum value is 7 ).

In this case, if we further divide the structure of the ending part, we can regard the ending number as an integer composed of three parts:

1 ~ Byte 4 (32 bits) represents an integer, indicating the low part of the ending number.

5 ~ The 8-byte (32-bit) represents an integer that represents the middle part of the ending number.

9 ~ Byte 12 (32 bits) represents an integer, indicating the high part of the ending number.

In this way, the decimal ending number of an integer is divided into three integers.

Index and symbol of decimal

It is worth mentioning that the index part is also an integer first, but if we further observe the decimal structure, we can also find the form of the index part (000e eeee) it is strange that only five digits are valid because the maximum value is only 28. The reason for this is actually very simple. The base number of the decimal index is 10, the ending part represents a 29-or 28-bit integer (the reason is that the value of the highest 29 is actually only 7, therefore, only 28-bit values can be set at will ). Assume that we have a 28-digit decimal integer. The value in the 28 positions can be 0 ~ In this case, the decimal index controls the decimal point on which the 28-digit integer is to be given the decimal point.

Of course, we also need to remind readers that the negative exponential power of the decimal index is represented by the following values:

**Symbol * Number of tails/10 ^ Index**

Therefore, the decimal value range is-/+ 79228162514264337593543950335, however, it is precisely because decimal can represent the effective digits of decimal numbers in the range of 28 or 29 (depending on whether the maximum value is within 7). Therefore, when decimal is expressed, the number of decimal places is also limited.

Four integers in decimal

Let's take a look at the decimal structure again. We can find that only 128 bits in 102 are required. In addition to the meaningful 102 bits, the values of the other bits are 0. We can further divide the 102 bits into four integers. This is an int array containing four elements returned when we call the decimal. GetBits (value) method:

The first three int integers, as I have mentioned above, are used to represent the middle and high parts of the low part of the ending number.

The last int integer represents the index and symbol. 0 ~ 15 digits are not used, but all are set to 0; 16 ~ 23 bits are used to represent the index. Of course, because the maximum value of the index is 28, only 5 of them are valid; 24 ~ The 30 digits are not used, but all are set to 0. The last digit is the symbol bit. 0 indicates a positive number, and 1 indicates a negative number.

Here is an example:

// Get the composition of decimal
using System;
using System.Collections.Generic;
class Test
{
static void Main ()
{
decimal [] vals = {1.111111m, -1.111111m};
Console.WriteLine ("{0,31} {1,10: X8} {2,10: X8} {3,10: X8} {4,10: X8}",
"Argument", "Bits [3]", "Bits [2]", "Bits [1]",
"Bits [0]");
Console.WriteLine ("{0,31} {1,10: X8} {2,10: X8} {3,10: X8} {4,10: X8}",
"--------", "-------", "-------", "-------",
"-------");
foreach (decimal val in vals)
{
int [] bits = decimal.GetBits (val);
Console.WriteLine ("{0,31} {1,10: X8} {2,10: X8} {3,10: X8} {4,10: X8}", val, bits [3], bits [2 ], bits [1], bits [0]);
}
}
}

The result of compiling and running this code is as follows:

0x03 how to avoid "errors"

Through the previous paragraph, I believe that readers should have discovered that decimal is actually not mysterious. Therefore, we are more confident that the correct answer will be obtained when decimal is used for decimal calculation. However, as I mentioned above, although decimal improves the accuracy of computing, its effective digits are also limited. Especially when it is used to represent decimal places, if the number of digits exceeds the valid number of digits, the answer may be "Incorrect.

For example, the following small example:

// Errors caused by not paying attention to significant digits
using System;
class Test
{
static void Main ()
{
var input = 1.1111111111111111111111111111m;
for (int i = 1; i <10; i ++)
{
decimal output = input * (decimal) i;
Console.WriteLine (output);
}
}
}

Let's compile and run it:

It can be found that the result within 7 is correct, but the last part multiplied by 8 and multiplied by 9 has an error. The reason for this result is that I have mentioned more than once in the above article, that is, in the case of 29 valid digits, the maximum value cannot exceed 7 to obtain accurate values. Multiply by 8 and multiply by 9 obviously do not meet this requirement.

Therefore, in combination with my previous article "compromise and trade-offs, deconstruct of decimal operations in C #", we can summarize the following two methods in computer to reduce decimal errors:

1. avoidance strategy: Ignore these errors. Sometimes some errors are acceptable based on different program purposes. This is also well understood, and the error is also common in daily life within a allowed range.

2. Convert decimals into integers for calculation: Since the computer may have errors when using binary for decimal computation, there is generally no problem when calculating integers. Therefore, an integer can be used for decimal calculation, but the final result can be expressed as a decimal number.