Calculate the circumference

Original author: Bruce Dawson
  • Transfer
“Please write a function in C ++ that receives the diameter of the circle as a float and returns the length of the circle as a float.”

Sounds like a task in the first week of a C ++ course. But this is only at first glance. Difficulties arise already at the first stages of solving a problem. I propose to consider several approaches.

Student: How do you like this option?

#include 
float CalcCircumference1(float d)
{
    return d * M_PI;
}

Teacher: Yes, this code can compile normally. Or maybe not. M_PI is not defined in C or C ++ standards. This will work with the compiler in VC ++ 2005, but for later versions, you will have to use #define _USE_MATH_DEFINES before enabling math.h to access this non-standard constant. And as a result, you will write code that other compilers may not be able to handle.

Scene two


Student: Thank you for your wisdom, teacher. I removed the dependency on the non-standard M_PI constant. That's better?

float CalcCircumference2(float d)
{
    return d * 3.14159265358979323846;
}

Teacher: Yes, that’s better. This code will be compiled and you will get the desired result. But your code is inefficient. You multiply a single-precision number by a double-precision constant. The compiler will have to cast the function parameter of type float to type double, and then perform the inverse transformation to get the return value. If you compile code for SSE2, then this adds two instructions to the dependency chain and the calculations can take three times longer! In most cases, such delays are quite acceptable, but in the internal cycle the negative effect can be very significant.

If you are compiling for the x87 platform, then converting to a double type does not cost anything, but the inverse conversion is expensive - so expensive that some optimizing compilers throw away this conversion, and as a result you can get EXTREMELY unexpected results - for example, CalcCircumference (r) == CalcCircumference (r) will return false!

Scene three


Student: Thank you, teacher. Honestly, I don't know what SSE2 and x87 are, but I see how elegant the code becomes when the types are consistent. This is real poetry. I will use the single precision constant. How do you like this?

float CalcCircumference3(float d)
{
    return d * 3.14159265358979323846f;
}

Teacher: Yes, excellent! The symbol "f" at the end of the constant changes everything. If you looked at the generated machine code, you would understand that this option is much more compact and efficient. However, I have comments on the style. Don't you think that this mysterious constant has no place inside the function? Even if this number is Pi, the value of which is unlikely to change, it is better to assign a name to the constant and place it in the header file.

Scene four


Student: Thank you. You explain everything very intelligibly. I will put the line of code below in the general header file and will use it in my function. Is that okay?

const float pi = 3.14159265358979323846f;

Teacher: Yes, great! Using the “const” keyword, you indicated that the variable should not and cannot be changed, in addition, it can now be placed in the header file. But, I'm afraid, now we have to delve into some of the subtleties of defining scope in C ++.

By declaring pi with the const keyword, you get the static keyword effect as a bonus. For integer types, this is normal, but if you are dealing with a different data type (floating-point number, array, class, structure), memory for your variable can be allocated separately in each translation unit, which includes your header file. In some cases, you will end up with several tens or even hundreds of instances of a variable of type float and your executable will be unreasonably large.

Scene five


Student: Are you kidding? And what to do?

Teacher: Yes, we are still far from ideal. You can put the __declspec (selectany) or __attribute __ (weak) attribute on a constant declaration so that VC ++ and GCC, respectively, understand that it is enough to save one of the many copies of this constant. But since you and I are in the idealistic world of science, I insist on using standard C ++ constructs.

Scene six


Student: So something like this? Using constexpr from C ++ 11?

constexpr float pi = 3.14159265358979323846f;

Teacher: Yes. Now your code is perfect. Of course, VS 2013 will not be able to compile it, because it does not know what to do with constexpr. But you can always use the Visual C ++ Compiler Nov 2013 CTP toolkit or the latest version of GCC or Clang.

Student: Can #define be used?

Teacher: No!

Student: Ah, to hell with all this! Better I become a barista .

Scene seven


Student: Stop, I remember something. It's that simple! This is how the code will look:

mymath.h:
extern const float pi;
mymath.cpp:
extern const float pi = 3.14159265358979323846f;

Teacher: Exactly, in most cases it will be the right decision. But what if you are working on a DLL, how will external functions access mymath.h in your DLL? In this case, you will have to provide export and import of this symbol.

The problem is that the rules for integer types are completely different. It is advisable and recommended to add the following to the C ++ header file:

const int pi_i = 3;

The Pi number is not specified exactly enough, but the fact is that the integer constants in the header files do not require memory allocation, unlike other constants. The reason for this difference is not entirely clear, but most often it is not important.

I found out what “static” means in “const” several years ago when I was asked to find out why one of our key DLLs suddenly gained 2 MB in weight. It turns out that there was an array of constants in the header file, and we got thirty copies of this array in the DLL. That is, sometimes it still matters.

And yes, I still think that #define is a terrible choice in this case. Maybe this is not the worst solution, but I do not like it at all. I once encountered compilation errors caused by a pi declaration using #define. Pleasant not enough, I tell you! Littering the namespace is the main reason why you should avoid #define as much as possible.

Conclusion


I don’t know exactly what lesson we have learned from all this. The essence of the problem that arises when we declare a constant like float or double in a header file or a structure or an array of constants is far from clear to everyone. In most serious programs, because of this, duplicates of static constants arise, and sometimes they are unreasonably large. I suppose constexpr can save us from this problem, but I do not have enough experience using it to know for sure.

I came across programs that were hundreds of kilobytes larger than their “real” size - and all because of the array of constants in the header file. I also saw a program that ultimately ended up with 50 copies of the class object (plus another 50 calls to the constructors and destructors), because this class object was defined as a const type in the header file. In other words, there is something to think about.

You can see how this happens with GCC by downloading a test program from here . Build it with the make command, and then run objdump -d constfloat | grep flds to find four read instructions from adjacent addresses in the data segment. If you want to take up more space, add the following to header.h:

const float sinTable[1024] = { 0.0, 0.1, };

In the case of GCC, the increase will be 4 KB per conversion record (source file), that is, the executable file will grow by 20 KiB, even if no one accesses the table once.

As usual, operations with floating-point numbers are associated with significant difficulties, but in this case, it seems to me, the too slow evolution of the C ++ language is to blame.

What else to read on the topic:


VC ++: how to avoid duplication and how to avoid duplication

The compiler in VC ++ 2013 Update 2 introduced the / Gw option, which puts each global variable in a separate COMDAT container, allowing the linker to identify and get rid of duplicates. Sometimes this approach helps to avoid the negative consequences of declaring constants and static variables in header files. In Chrome, such changes helped save about 600 KB ( details ). Part of this savings was achieved (surprise!) By removing thousands of instances of twoPiDouble and piDouble (as well as twoPiFloat and piFloat).

However, in VC ++ 2013 STL, there are several objects declared as static or const in a class declaration that / Gw cannot delete. All these objects occupy one byte, but in the end it runs over 45 kilobytes. I informed the developers about this error and received a response that it was fixed in VC ++ 2015.

At the request of the author, we share the link to the original here

Also popular now: