Làm thế nào để tìm các hoạt động sao chép giả C ++?


11

Gần đây, tôi đã có những điều sau đây

struct data {
  std::vector<int> V;
};

data get_vector(int n)
{
  std::vector<int> V(n,0);
  return {V};
}

Vấn đề với mã này là khi cấu trúc được tạo, một bản sao xảy ra và giải pháp thay vào đó là viết return {std :: move (V)}

Có kẻ nói dối hoặc phân tích mã sẽ phát hiện các hoạt động sao chép giả như vậy không? Cả cppcheck, cpplint, hay clang-tidy đều không thể làm được.

EDIT: Một số điểm để làm cho câu hỏi của tôi rõ ràng hơn:

  1. Tôi biết rằng một hoạt động sao chép đã xảy ra bởi vì tôi đã sử dụng trình biên dịch explorer và nó hiển thị một lệnh gọi tới memcpy .
  2. Tôi có thể xác định rằng một hoạt động sao chép xảy ra bằng cách nhìn vào tiêu chuẩn có. Nhưng ý tưởng sai lầm ban đầu của tôi là trình biên dịch sẽ tối ưu hóa bản sao này. Tôi đã sai.
  3. Đây có thể không phải là vấn đề của trình biên dịch vì cả clang và gcc đều tạo mã tạo ra một memcpy .
  4. Memcpy có thể rẻ, nhưng tôi không thể tưởng tượng được tình huống sao chép bộ nhớ và xóa bản gốc rẻ hơn so với việc chuyển một con trỏ bằng std :: move .
  5. Việc thêm std :: move là một hoạt động cơ bản. Tôi sẽ tưởng tượng rằng một bộ phân tích mã sẽ có thể đề xuất sự điều chỉnh này.

2
Tôi không thể trả lời liệu có tồn tại bất kỳ phương pháp / công cụ nào để phát hiện các hoạt động sao chép "giả" hay không, tuy nhiên, theo quan điểm trung thực của tôi, tôi không đồng ý rằng việc sao chép std::vectorbằng bất kỳ phương tiện nào không phải là mục đích của nó . Ví dụ của bạn hiển thị một bản sao rõ ràng và nó chỉ là tự nhiên và cách tiếp cận đúng, (một lần nữa imho) để áp dụng std::movechức năng như bạn tự đề xuất nếu một bản sao không phải là điều bạn muốn. Lưu ý rằng một số trình biên dịch có thể bỏ qua việc sao chép nếu cờ tối ưu hóa được bật và vectơ không thay đổi.
Magnus

Tôi sợ có quá nhiều bản sao không cần thiết (có thể không ảnh hưởng) để làm cho quy tắc kẻ nói dối này có thể sử dụng được: - / ( rỉ sét sử dụng di chuyển theo mặc định nên yêu cầu sao chép rõ ràng :))
Jarod42

Các đề xuất của tôi về tối ưu hóa mã về cơ bản là để phân tách chức năng bạn muốn tối ưu hóa và bạn sẽ khám phá các hoạt động sao chép bổ sung
camp0

Nếu tôi hiểu chính xác vấn đề của bạn, bạn muốn phát hiện các trường hợp trong đó một thao tác sao chép (hàm tạo hoặc toán tử gán) được gọi trên một đối tượng theo sau sự phá hủy của nó. Đối với các lớp tùy chỉnh, tôi có thể tưởng tượng việc thêm một số cờ gỡ lỗi được đặt khi một bản sao được thực hiện, đặt lại trong tất cả các hoạt động khác và kiểm tra hàm hủy. Tuy nhiên, không biết cách làm tương tự đối với các lớp không tùy chỉnh trừ khi bạn có thể sửa đổi mã nguồn của chúng.
Daniel Langr

2
Kỹ thuật tôi sử dụng để tìm các bản sao giả là tạm thời đặt công cụ tạo bản sao ở chế độ riêng tư, sau đó kiểm tra xem trình biên dịch bị lỗi do hạn chế truy cập. (Mục tiêu tương tự có thể đạt được bằng cách gắn thẻ trình tạo bản sao là không dùng nữa, đối với trình biên dịch hỗ trợ gắn thẻ như vậy.)
Eljay

Câu trả lời:


2

Tôi tin rằng bạn có quan sát chính xác nhưng giải thích sai!

Việc sao chép sẽ không xảy ra bằng cách trả về giá trị, bởi vì mọi trình biên dịch thông minh thông thường sẽ sử dụng (N) RVO trong trường hợp này. Từ C ++ 17, điều này là bắt buộc, vì vậy bạn không thể thấy bất kỳ bản sao nào bằng cách trả về một vectơ được tạo cục bộ từ hàm.

OK, hãy chơi một chút với std::vectorvà những gì sẽ xảy ra trong quá trình xây dựng hoặc bằng cách điền từng bước một.

Trước hết, hãy tạo một kiểu dữ liệu giúp mọi bản sao hoặc di chuyển hiển thị như thế này:

template <typename DATA >
struct VisibleCopy
{
    private:
        DATA data;

    public:
        VisibleCopy( const DATA& data_ ): data{ data_ }
        {
            std::cout << "Construct " << data << std::endl;
        }

        VisibleCopy( const VisibleCopy& other ): data{ other.data }
        {
            std::cout << "Copy " << data << std::endl;
        }

        VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
        {
            std::cout << "Move " << data << std::endl;
        }

        VisibleCopy& operator=( const VisibleCopy& other )
        {
            data = other.data;
            std::cout << "copy assign " << data << std::endl;
        }

        VisibleCopy& operator=( VisibleCopy&& other ) noexcept
        {
            data = std::move( other.data );
            std::cout << "move assign " << data << std::endl;
        }

        DATA Get() const { return data; }

};

Và bây giờ hãy bắt đầu một số thử nghiệm:

using T = std::vector< VisibleCopy<int> >;

T Get1() 
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
    std::cout << "End init" << std::endl;
    return vec;
}   

T Get2()
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec(4,0);
    std::cout << "End init" << std::endl;
    return vec;
}

T Get3()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

T Get4()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.reserve(4);
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

int main()
{
    auto vec1 = Get1();
    auto vec2 = Get2();
    auto vec3 = Get3();
    auto vec4 = Get4();

    // All data as expected? Lets check:
    for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}

Những gì chúng ta có thể quan sát:

Ví dụ 1) Chúng tôi tạo một vectơ từ danh sách khởi tạo và có thể chúng tôi hy vọng rằng chúng tôi sẽ thấy cấu trúc 4 lần và 4 lần di chuyển. Nhưng chúng tôi nhận được 4 bản! Nghe có vẻ hơi bí ẩn, nhưng lý do là việc thực hiện danh sách khởi tạo! Đơn giản là nó không được phép di chuyển khỏi danh sách vì iterator từ danh sách là const T*điều khiến cho không thể di chuyển các phần tử từ nó. Một câu trả lời chi tiết về chủ đề này có thể được tìm thấy ở đây: initizer_list và di chuyển ngữ nghĩa

Ví dụ 2) Trong trường hợp này, chúng tôi nhận được một bản dựng ban đầu và 4 bản sao của giá trị. Điều đó không có gì đặc biệt và là những gì chúng ta có thể mong đợi.

Ví dụ 3) Cũng ở đây, chúng tôi xây dựng và một số di chuyển như mong đợi. Với việc thực hiện stl của tôi, vectơ tăng theo hệ số 2 mỗi lần. Vì vậy, chúng ta thấy một cấu trúc đầu tiên, một cấu trúc khác và vì vectơ thay đổi kích thước từ 1 đến 2, chúng ta thấy sự di chuyển của phần tử đầu tiên. Trong khi thêm 3 cái, chúng ta thấy thay đổi kích thước từ 2 thành 4 cần di chuyển hai yếu tố đầu tiên. Tất cả như mong đợi!

Ví dụ 4) Bây giờ chúng tôi dự trữ không gian và điền vào sau. Bây giờ chúng tôi không có bản sao và không di chuyển nữa!

Trong mọi trường hợp, chúng tôi không thấy bất kỳ động thái nào cũng như sao chép bằng cách trả lại vectơ cho người gọi! (N) RVO đang diễn ra và không cần thực hiện thêm hành động nào trong bước này!

Quay lại câu hỏi của bạn:

"Cách tìm các hoạt động sao chép giả của C ++"

Như đã thấy ở trên, bạn có thể giới thiệu một lớp proxy ở giữa cho mục đích gỡ lỗi.

Làm cho copy-ctor private có thể không hoạt động trong nhiều trường hợp, vì bạn có thể có một số bản sao mong muốn và một số bản sao bị ẩn. Như trên, chỉ có mã ví dụ 4 sẽ hoạt động với một copy-ctor riêng! Và tôi không thể trả lời câu hỏi, nếu ví dụ 4 là câu hỏi nhanh nhất, vì chúng ta lấp đầy hòa bình bằng hòa bình.

Xin lỗi rằng tôi không thể cung cấp một giải pháp chung cho việc tìm các bản sao "không mong muốn" ở đây. Ngay cả khi bạn đào mã của mình cho các cuộc gọi memcpy, bạn sẽ không tìm thấy tất cả vì nó cũng memcpysẽ được tối ưu hóa và bạn sẽ thấy trực tiếp một số hướng dẫn trình biên dịch thực hiện công việc mà không cần gọi đến memcpychức năng thư viện của bạn .

Gợi ý của tôi là không tập trung vào một vấn đề nhỏ như vậy. Nếu bạn có vấn đề về hiệu suất thực sự, hãy lấy một hồ sơ và đo lường. Có rất nhiều kẻ giết người hiệu suất tiềm năng, rằng đầu tư nhiều thời gian vào memcpyviệc sử dụng giả có vẻ không phải là một ý tưởng đáng giá.


Câu hỏi của tôi là loại học thuật. Vâng, có rất nhiều cách để có mã chậm và đây không phải là vấn đề ngay lập tức đối với tôi. Tuy nhiên, chúng ta có thể tìm thấy các hoạt động memcpy bằng cách sử dụng trình thám hiểm trình biên dịch. Vì vậy, chắc chắn có một cách. Nhưng nó chỉ khả thi cho các chương trình nhỏ. Quan điểm của tôi là có sự quan tâm của mã sẽ tìm thấy các đề xuất về cách cải thiện mã. Có các máy phân tích mã tìm thấy lỗi và rò rỉ bộ nhớ, tại sao không phải là vấn đề như vậy?
Mathieu Dutour Sikiric

"mã sẽ tìm thấy đề xuất về cách cải thiện mã." Điều đó đã được thực hiện và thực hiện trong chính trình biên dịch. (N) Tối ưu hóa RVO chỉ là một ví dụ duy nhất và hoạt động hoàn hảo như được hiển thị ở trên. Bắt memcpy không giúp ích gì khi bạn đang tìm kiếm "memcpy không mong muốn". "Có các máy phân tích mã tìm thấy lỗi và rò rỉ bộ nhớ, tại sao không phải là vấn đề như vậy?" Có lẽ nó không phải là một vấn đề (phổ biến). Và công cụ tổng quát hơn để tìm các vấn đề "tốc độ" cũng đã có mặt: profiler! Cảm nhận cá nhân của tôi là, bạn đang tìm kiếm một thứ học thuật không phải là vấn đề trong phần mềm thực sự ngày nay.
Klaus

1

Tôi biết rằng một hoạt động sao chép đã xảy ra bởi vì tôi đã sử dụng trình biên dịch explorer và nó hiển thị một lệnh gọi tới memcpy.

Bạn đã đưa ứng dụng hoàn chỉnh của mình vào trình thám hiểm trình biên dịch và bạn đã kích hoạt tối ưu hóa chưa? Nếu không, thì những gì bạn thấy trong trình thám hiểm trình biên dịch có thể hoặc không thể là những gì đang xảy ra với ứng dụng của bạn.

Một vấn đề với mã bạn đã đăng là trước tiên bạn tạo một mã std::vector, sau đó sao chép nó vào một thể hiện của data. Sẽ tốt hơn khi khởi tạo data với vectơ:

data get_vector(int n)
{
  return {std::vector<int> V(n,0)};
}

Ngoài ra, nếu bạn chỉ cung cấp cho trình biên dịch trình thám hiểm định nghĩa dataget_vector(), và không có gì khác, thì nó phải mong đợi điều tồi tệ hơn. Nếu bạn thực sự cung cấp cho nó một số mã nguồn sử dụng get_vector() , thì hãy xem tập hợp nào được tạo cho mã nguồn đó. Xem ví dụ này để biết những gì sửa đổi ở trên cộng với việc sử dụng thực tế cộng với tối ưu hóa trình biên dịch có thể khiến trình biên dịch tạo ra.


Tôi chỉ đưa vào máy tính thám hiểm mã ở trên (có memcpy ) nếu không câu hỏi sẽ không có ý nghĩa. Điều đó được nói rằng câu trả lời của bạn là tuyệt vời trong việc hiển thị các cách khác nhau để tạo ra mã tốt hơn. Bạn cung cấp hai cách: Sử dụng tĩnh và đặt hàm tạo trực tiếp vào đầu ra. Vì vậy, những cách đó có thể được đề xuất bởi một bộ phân tích mã.
Mathieu Dutour Sikiric
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.