A little about “PI” and other built-in constants

To remember the “Pi” number,
Must read correctly:
Three, fourteen, fifteen
ninety two and six

folk wisdom

No, no, I’m not going to tell all the jokes about constants, like how the number E and the year of birth of Leo Tolstoy are connected. It’s about something else.
Once one of my colleagues asked me to look at his program with a “fresh look”. He did a test calculation, and the result should have been an identity matrix. In place of the zero elements, there were values ​​close to zero – something around 10**-17, which can be explained by the calculation error and the initial data. But three elements had a value of 10**-7. The question was, and, in fact, why so? because all formulas are “symmetrical”.

The analysis showed that the “copy-paste” fragment, in which the operator PI=3.1415 turned out to be, is to blame.
I immediately remembered a quote from great book Sternberg:

The second typical error is illustrated by an example
Z=3.14*COS(2X);
in which the constant 3.14 is intended to represent the mathematical constant “pi”. But this constant defines “pi” with a huge error of 0.00159…, which we run into our calculation. It is difficult to predict what this error will grow into after going through the calculation.
Here you also need to create a variable in which to write the value of the constant with the maximum possible accuracy, and then use a shorter identifier instead of a long value.

Those. the constant was specified with insufficient accuracy, which caused unequal resulting values ​​of the matrix elements.

And then, in order to exclude such cases in the future, I decided to introduce a built-in constant π into the compiler and language. And act in the spirit of a joke about a graduate of the Faculty of Physics and Technology, who, when asked how much it would be twice two, irritably stated: Am I supposed to remember all the constants?
No need to remember the value of π, it should appear in the program “by itself”.

It would seem, what is simpler: take a clean pan header file and append it to an initialized static variable. Once writing out the most accurate value of π.

Well, I don’t like header files. They often add too much to the program. And there were no header files in the colleague’s program. In addition, I accompany the compiler from the PL / 1 language, which means, unlike most programmers, I can implement non-trivial solutions, including changing and supplementing the language itself through compiler changes.

Scary tales about the incredible complexity of the PL/1 language have come down to us from the distant 60s (which is ridiculous against the background of some C ++). One evidence of this complexity was the large number of functions built into the language, which allegedly complicated the compiler. However, most of the built-in functions do not complicate the compiler in any way, it simply has a list of all built-in objects inside itself (analogous to a header file) and, at the beginning of its work, transfers this list to the outermost program block created by the compiler. The block structure of PL/1 helps a lot here.

For example, the built-in function sin to the compiler, it is no different from any function declared by the programmer, except that it does not need to be explicitly declared.

And if the program describes its own sin, then the default will not be called. And only if you always need to call the standard, then only then you need to specify a description sin with attribute built in. Anyway, you don’t have to write something like std::sin (or rather math::sin), which, in my opinion, ugly inflates the original text.

So, all the functions built into the language are located in the most external and directly inaccessible block of the program for programmers. Our compiler even deliberately left an empty space at the end of the built-in list, and you can use a primitive program to introduce new built-in objects without retranslating the compiler itself.

Along with built-in functions, there are also built-in variables. Usually these are constants-variables. Despite the obvious idiocy of this term, we mean constants for which memory is allocated by the compiler, unlike, for example, most literals.

For example, the built-in constant ?FILE is filled by the compiler with the name of the source file, and the constant ?LINE – the current line number of the source text. After that, debug print as always the same type statement put skip list(?file, ?line,’error’); will produce different values ​​in the log in different places in the source texts.

But there are in the list and not constants. For example, for I / O arrays, the compiler creates and immediately compiles loops on the fly, where these built-in variables are used, as loop variables.

Ever since the days of Harry Kildal, an unofficial rule has been adopted in the compiler: identifiers of all service objects must begin with the character “?”. In fact, this sign is allowed by the language standard in any identifiers. But it is very convenient just to categorically not recommend users to start their names with this character, and then there will never be confusion between programmer and service objects.

Thus, I simply add a variable to the list of built-in objects in the compiler ?PI type double or, in terms of the language, external float(53). It remains only to fill it with a value.

There is a funny problem here. If a specific program does not use the new built-in constant ?PI, the compiler throws it out of the object module. It would seem that then it is possible to fill in the value already at the start of the program, for example, in the standard prologue. There is still a lot of work done without it.

But then this constant needs to be described in the source text of the prologue itself, and it turns out that ?PI is always required, even if the program itself does not refer to it.

The best option is to fill while the link editor is running. It still has to fill in some constants, like the built-in constant ?DATE, which takes the value of the current build date. In principle, the compiler cannot fill this constant, since the assembly can take place much later than the compilation of individual modules.

It turns out very elegantly: if the modules did not access ?PI, then the link editor will not find it in its tables and will not fill in anything in the assembled program. If at least one of the separately compiled modules called ?PIthis constant will be stored in the compiled EXE file, and the editor will fill it with the value π.

Well, and, of course, the compiler must ensure that the program does not try to write anything to ?PIdespite the fact that, as you know, in wartime, the value of the sine can reach 4, and π – 10.

And, finally, the icing on the cake is the possibility of a simple “tactical optimization” of the code in terms of the constant ?PI.
If, for example, the program has a fragment:

dcl x float(53);
...
if x*?pi>1e0 then …
...

it compiles to code like:

BB00000000                  mov  q rbx,offset @?PI
BAE0000000                  mov  q rdx,offset X
F9                          stc
DD0353DC0ADD1C24            call   ?FM4_M
BBF0000000                  mov  q rbx,offset @000000F0h
E800000000                  call   ?FC44L
7E05                        jle    @1

Where ?FM_M – this is an “extra code”, i.e. service call, in this case substituted in line.
In the debugger, this fragment looks like this:

And you can apply the simplest optimization, which is that if the compiler generated the command FLD64 [RBX]then the compiler looks to see if there was a command a little earlier MOV EBX,OFFSET?PI.
If there is, instead of the command FLD you can put the command FLDPI direct loading π, i.e. just replace two bytes of codes DD03 by two bytes D9EB:

It would seem that there is no difference – after all, the amount of code has not even changed. However, this substitution has two advantages:

a) Instead of loading 8 bytes of a variable ?PI V FPU the hardware constant π is loaded with the highest possible accuracy for the x86 FPU – 80 bits (and again, this constant does not need to be remembered by yourself). Incidentally, such an internal value of π in the FPU is 4×0.C90FDAA22168C234Ch. It is this mess of zeros and ones that is sometimes used as part of the code for RSA-keys.

b) When executing a command FLDPI there is no access to the memory that stores the constant ?PIwhich is important for modern processors and speeds up their work.
Of course, in this case, it would be possible to throw out the entire team altogether. MOV EBX,OFFSET?PIbut since the optimization is already in progress at this point, perhaps the value of the register RBX further used. It is better not to complicate the analysis with unnecessary checks and the risk of error.

And now my colleague is left to wish to forget about all the representations of π and use only the built-in constant in the calculations.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *