Tiny Steps

Learn from tiny steps every day, and make a big difference in your journey

Return value optimization là một khái niệm liên quan đến compiler optimization technique, compiler sẽ tự động xóa bỏ các việc copying objects không cần thiết ghi chúng ta trả ra một object từ hàm.

Đặt vấn đề

Khi viết một function trong C++ chúng ta thường có 2 cách cơ bản trả về một đối tượng trong hàm, như sau:

Cách 1: Trả về trực tiếp từ hàm

Dog func1(){
    return Dog();
};

Cách 2: Tạo một đối tượng từ ngoài hàm và truyền vào một reference để gán giá trị

void func2(Dog& d){
    d = Dog();
}

Mình tin rằng, nếu là một lập trình viên C++ có kinh nghiệm về move semantics từ C++11 chắc chắn bạn sẽ thường làm theo cách thứ hai để tránh việc copying objects khi trả về đối tượng từ hàm.

Nhưng không, trong bài viết này mình sẽ giới thiệu với các bạn về Return Value Optimization (RVO). Để tránh phát sinh chi phí và gia tăng hiệu năng, trình biên dịch có thể tự động tối ưu hóa việc copying objects, khi đó chúng ta có thể thực hiện “copy” mà thực ra lại không hề copy, và chúng ta có thể implement function theo Cách 1 để code đơn giản, dễ hiểu và vẫn đảm bảo hiệu năng.

Giải thích

Bây giờ hãy chạy đoạn code theo cách 1 để kiểm chứng nhé:

class Dog{
public:
    Dog(){
        std::cout << "Constructor.\n";
    }
    Dog(const Dog& d){
        std::cout << "Copy Constructor.\n";
    }
    Dog(Dog&& d){
        std::cout << "Move Constructor.\n";
    }

    Dog& operator= (const Dog& d){
        std::cout << "Copy Assignment.\n";
        return *this;
    }
    Dog& operator=(Dog&& d){
        std::cout << "Move Assignment.\n";
        return *this;
    }
};
Dog func1(){
    return Dog();
};
int main(){
    auto d = func1();
}

Trước tiên, hãy biên dịch và sử dụng RVO nhé, ở đây mình sử dụng GCC 9.4, các bạn biên dịch bình thường mà không thêm cờ nào thì trình biên dịch sẽ tự động có RVO, và kết quả sẽ như sau:

Kết quả rất bất ngờ đúng không, không hề có sự copy nào ở đây cả, vì trình biên dịch đã tự động tối ưu việc đó cho chúng ta

Còn bây giờ, hãy thử disable RVO đi nhé, các bạn chỉ cần thêm cờ -fno-elide-constructors khi compile.

Wow, kết quả là đoạn kết trên sẽ copy 2 lần và đương nhiên sẽ ảnh hưởng đến hiệu năng nếu như hàm Copy constructor hoặc Move constructor chúng ta transfer nhiều data.

Như vậy đương nhiên chúng ta có thể implement function theo cách một và vẫn đảm bảo High Performance Coding, vì mặc định trình biên dịch đã làm điều đó cho chúng ta.

Bây giờ hãy đi vào chi tiết xem compiler sẽ giải quyết việc tối ưu giá trị trả về như thế nào nhé, về cơ bản sẽ có 2 loại optimizations mà compiler sẽ áp dụng, đó là:

  • RVO (Return Value Optimization): Nó sẽ tự động tối ưu khi đối tượng được tạo ra từ bên trong hàm là temporary và là đối tượng không tên, ví dụ:
Dog func1(){
    return Dog();
};

  • NRVO (Named Return Value Optimization): như một bản nâng cấp của RVO, nó có thể loại bỏ các temporary objects ngay cả khi objects được trả về có tên, ví du:
Dog func1(){
    auto d = Dog();
    return d;
};

Vậy bản chất Return Value Optimization hoặc động như thế nào?

Ý tưởng của cả hai optimizations trên là compiler sẽ sử dụng memory space của object được gán bên ngoài hàm, để khởi tạo trực tiếp cho object được tạo bên trong hàm mà nó có đối tượng trả về. Bằng cách này nó sẽ giảm bớt được việc copy các temporary objects.

Chúng ta cần làm gì để đảm bảo Compiler sẽ thực hiện Return Value Optimization.

1. Chỉ áp dụng cho các functions có 1 object được return trong xuyên suốt function đó, ví dụ

Dog func()
{
    Dog d;
    if (conditional)
    {
        return d;
    }
    ....
    return d;
}

2. Chỉ gọi function và gán cho một biến được tạo mới, và không được gán biến đó cho function tiếp theo, ví dụ:

// Applied RVO
Dog d = func1();
// RVO will not happen
d = func1();

Trường hợp này Move Semantics sẽ được sử dụng

Kết luận

Như vậy trong bài viết này, mình đã giới thiệu cho các bạn về Return Value Optimization, hi vọng nó sẽ giúp các bạn trong việc cải thiện hiệu năng của chương trình và vẫn giữ code đơn giản, dễ hiểu, nhưng hãy nhớ là hãy tuân thủ cá quy tắc để đảm bảo compiler luôn áp dụng RVO nhé.

Hi vọng bài viết hữu ích cho các bạn, nếu các bạn thấy bài viết hay thì hãy chia sẻ cho các anh em lập trình khác cùng biết nhé. Cám ơn các bạn đã ghé đọc ^^. Chúc các bạn một ngày tốt lành ❤

Bình luận về bài viết này