简体   繁体   中英

Microblaze & C++ | Why does the code size increase dramatically under certain conditions?

I have been developing embedded software for the Microblaze processor for more than a year using C++. My designs were not so complex, so I wasn't using the powerful, object-oriented features of the language.

For a while, I have been trying to enhance the structure of my designs. For this purpose, I try to widely use the sophisticated features of C++ such as inheritance, polymorphism, etc. As a newbie, I believe that using inheritance solely doesn't affect the code size. Only the polymorphism has some side effects like adding virtual table pointers, run-time-type-informations, etc. My problem started with adding a pure virtual member function to a base class.

To provide a runnable example, I will try to mimic the situation that I face against.

The code below compiles and produces 13292 bytes of code. There is no way that this code can have such an amount of instructions. But, I believe that there are some parts from the generated BSP that are mandatory to include when producing an elf file.

class Base{
public:
    Base() = default;
    ~Base() = default;
  
    virtual void func() {}
  
    int m_int;
};

class Derived : public Base{
public:
    Derived() = default;
    ~Derived() = default;
    
    void func() final {}
  
    int m_int2;
};

int main()
{
    Derived d;
  
    while(1);    
}
 

13KB is not that much when you think that you have nearly 128KB of usable RAM. Actually, I didn't even notice the size of the produced code until the problem with the pure virtual functions emerges. The second code, below, has the same structure except for the func() is now a pure virtual function. Building this code gives us a code size which more than the available*(128KB)* RAM size. So, I modified the linker file to add some fake RAM just to be able to compile the code. After a successful compilation, the size of the produced code is nearly 157KB!

class Base{
public:
    Base() = default;
    ~Base() = default;
  
    virtual void func() = 0;
  
    int m_int;
};

class Derived : public Base{
public:
    Derived() = default;
    ~Derived() = default;
    
    void func() final {}
  
    int m_int2;
};

int main()
{
    Derived d;
  
    while(1);    
}

I didn't change any preferences of the compiler, all arguments are in their default states. There are no additional libraries other than the auto-generated ones. What do you think that the problem could be?

Some Additional Notes

  • I tried the codes on two different IDEs. Vivado SDK 2017.2 and Vitis 2019.2
  • The same problem also goes for the dynamic allocation calls(operator new and delete). Replacing them with C-Style malloc and free solves the problem.
  • Building the codes in the release mode solves the problem also. In release mode, the produced code is 1900 bytes whether I use the pure virtual function or not.

I can provide additional information if needed, thanks

I asked the same question on Xilinx forums, you can find it here

The solution is a little bit creepy:) Before beginning, special thanks to everyone who helped.

SHORT ANSWER

Just add the following code piece to your main file:

extern "C" void __cxa_pure_virtual() { while(1); }

If you also want to solve the problem related to operator new and operator delete , add the following codes also:

void* operator new(const std::size_t size) noexcept
{
    void* p = std::malloc(size);
    return p;
}

void operator delete(void* p) noexcept
{
    std::free(p);
}

DETAILS

The original solution is here . The problem starts with completely pulling libstdc++ out of the picture. This way we waive the right of using standard library functions, so we should provide our own implementations of the standard calls such as malloc , new , free , etc. Even if you reimplement all required calls, the compiler would complain about the lack of a function called __cxa_pure_virtual() . This is a clue for the final solution.

The __cxa_pure_virtual function is an error handler that is invoked when a pure virtual function is called. We can easily say that we never make such foolish attempts. But, the compiler never trusts any software developer:) Therefore, when you write a C++ code that includes pure virtual functions, the compiler implicitly adds an error handler to handle potential runtime errors. As you can guess that those are expensive calls for systems with limited resources such as in our case the Microblaze.

So, if we are writing a C++ application that has pure virtual functions we shall supply our own __cxa_pure_virtual error handler function. If you are not a competitive embedded software developer you should just add an endless to your custom handler function. Don't worry, you will never have a chance to call your pure virtual function that invokes the error handler as long as you follow the best practices of the language.

The problem with the operator new and operator delete is also related to underlying exception mechanisms. To avoid expensive exception handling mechanisms you could just reimplement them in a way that doesn't throw any exception. The only thing that you should consider is to check the allocation success after calling the operator new as it will no more produce exceptions. I believe that you will never need to call the operator delete as long as you work on an operating systemless application project.

After applying this holy recipe on your own codes you will see that the size of the executable file will fall back to its original state.

The answer is open to contributions and suggestions. I would be appreciated if you could make so

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM