จะหาการคัดลอกปลอมแบบ C ++ ได้อย่างไร?


11

เมื่อเร็ว ๆ นี้ฉันมีดังต่อไปนี้

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

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

ปัญหาของรหัสนี้คือเมื่อโครงสร้างถูกสร้างสำเนาเกิดขึ้นและวิธีแก้ไขคือแทนที่จะเขียนreturn {std :: move (V)}

มีตัววิเคราะห์ linter หรือ code ที่ตรวจจับการดำเนินการคัดลอกปลอมหรือไม่ cppcheck, cpplint และ clang-tidy ไม่สามารถทำได้

แก้ไข: มีหลายประเด็นที่ทำให้คำถามของฉันชัดเจนขึ้น:

  1. ฉันรู้ว่าดำเนินการคัดลอกเกิดขึ้นเพราะผมเคยสำรวจคอมไพเลอร์และมันแสดงให้เห็นว่าการเรียกไปยังmemcpy
  2. ฉันสามารถระบุได้ว่าการคัดลอกเกิดขึ้นโดยดูจากมาตรฐานใช่ แต่ความคิดที่ผิดพลาดครั้งแรกของฉันคือคอมไพเลอร์จะทำสำเนานี้ให้เหมาะสมที่สุด ฉันผิดไป.
  3. มันเป็น (น่าจะ) ไม่เป็นปัญหาคอมไพเลอร์เนื่องจากทั้งสองเสียงดังกราวและ GCC รหัสผลิตผลที่ผลิตmemcpy
  4. memcpy อาจจะถูก แต่ฉันไม่สามารถจินตนาการสถานการณ์ที่คัดลอกหน่วยความจำและการลบเดิมมีราคาถูกกว่าการส่งผ่านตัวชี้โดยมาตรฐาน :: ย้าย
  5. การเพิ่มstd :: moveเป็นการดำเนินการเบื้องต้น ฉันคิดว่าตัววิเคราะห์รหัสจะสามารถแนะนำการแก้ไขนี้ได้

2
ฉันไม่สามารถตอบหรือไม่ว่ามีวิธีการใด ๆ ที่มีอยู่ / เครื่องมือสำหรับการตรวจสอบ "ปลอม" คัดลอกการดำเนินงานอย่างไรก็ตามในความซื่อสัตย์ของฉันผมไม่เห็นว่าการคัดลอกของstd::vectorโดยวิธีการใด ๆ ที่ไม่ได้เป็นสิ่งที่มันอ้างว่าจะเป็น ตัวอย่างของคุณแสดงสำเนาที่ชัดเจนและเป็นเรื่องธรรมดาและเป็นวิธีที่ถูกต้อง (อีกครั้งที่ imho) เพื่อใช้std::moveฟังก์ชั่นตามที่คุณแนะนำตัวเองหากสำเนาไม่ใช่สิ่งที่คุณต้องการ โปรดทราบว่าคอมไพเลอร์บางตัวอาจละเว้นการคัดลอกหากเปิดใช้งานการปรับแต่งค่าสถานะและเวกเตอร์ไม่เปลี่ยนแปลง
แมกนัส

ฉันกลัวว่าจะมีสำเนาที่ไม่จำเป็นมากเกินไป (ซึ่งอาจไม่ส่งผลกระทบ) เพื่อให้กฎ linter นี้ใช้งานได้: - / ( สนิมใช้การย้ายตามค่าเริ่มต้นดังนั้นต้องใช้สำเนาที่ชัดเจน :))
Jarod42

คำแนะนำของฉันสำหรับการเพิ่มประสิทธิภาพรหัสนั้นโดยทั่วไปจะแยกส่วนฟังก์ชั่นที่คุณต้องการเพิ่มประสิทธิภาพและคุณจะค้นพบการดำเนินการคัดลอกพิเศษ
camp0

หากฉันเข้าใจปัญหาของคุณอย่างถูกต้องคุณต้องการตรวจสอบกรณีที่มีการเรียกใช้การคัดลอก (ตัวสร้างหรือตัวดำเนินการกำหนดค่า) บนวัตถุตามด้วยการทำลาย สำหรับคลาสที่กำหนดเองฉันสามารถจินตนาการเพิ่มการตั้งค่าการดีบักบางอย่างเมื่อทำการคัดลอกรีเซ็ตในการดำเนินการอื่น ๆ ทั้งหมดและตรวจสอบใน destructor อย่างไรก็ตามไม่ทราบวิธีการทำเช่นเดียวกันสำหรับคลาสที่ไม่ได้กำหนดเองเว้นแต่คุณจะสามารถแก้ไขซอร์สโค้ดของพวกเขาได้
Daniel Langr

2
เทคนิคที่ฉันใช้ในการค้นหาสำเนาปลอมคือการทำให้ตัวสร้างสำเนาชั่วคราวเป็นการส่วนตัวและจากนั้นตรวจสอบที่คอมไพล์เลอร์ balks เนื่องจากข้อ จำกัด การเข้าถึง (วัตถุประสงค์เดียวกันสามารถทำได้โดยการแท็กตัวสร้างสำเนาเป็นคัดค้านสำหรับคอมไพเลอร์ที่สนับสนุนการติดแท็กดังกล่าว)
Eljay

คำตอบ:


2

ฉันเชื่อว่าคุณมีการสังเกตที่ถูกต้อง แต่การตีความผิด!

การคัดลอกจะไม่เกิดขึ้นโดยส่งคืนค่าเนื่องจากคอมไพเลอร์ฉลาดทั่วไปทุกรายจะใช้(N) RVOในกรณีนี้ ตั้งแต่ C ++ 17 นี่เป็นข้อบังคับดังนั้นคุณจะไม่เห็นสำเนาใด ๆ โดยส่งคืนเวกเตอร์ที่สร้างขึ้นในเครื่องจากฟังก์ชัน

ตกลงให้เล่นด้วยstd::vectorและสิ่งที่จะเกิดขึ้นในระหว่างการก่อสร้างหรือโดยการกรอกทีละขั้นตอน

ก่อนอื่นให้สร้างประเภทข้อมูลที่ทำให้ทุกสำเนาหรือย้ายปรากฏเช่นนี้

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; }

};

และตอนนี้เรามาเริ่มการทดลองกัน:

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; }
}

สิ่งที่เราสังเกตได้:

ตัวอย่างที่ 1) เราสร้างเวกเตอร์จากรายการ initializer และบางทีเราคาดว่าเราจะเห็นการสร้าง 4 ครั้งและการเคลื่อนไหว 4 ครั้ง แต่เราได้รับ 4 ชุด! ฟังดูลึกลับไปหน่อย แต่เหตุผลก็คือการใช้งาน initializer list! เพียงไม่อนุญาตให้ย้ายจากรายการเนื่องจากตัววนซ้ำจากรายการเป็นสิ่งconst T*ที่ทำให้ไม่สามารถย้ายองค์ประกอบออกจากรายการได้ คำตอบโดยละเอียดเกี่ยวกับหัวข้อนี้อยู่ที่นี่: initializer_list และย้ายซีแมนทิกส์

ตัวอย่างที่ 2) ในกรณีนี้เราจะได้รับการสร้างเริ่มต้นและมีค่า 4 ชุด นั่นคือไม่มีอะไรพิเศษและเป็นสิ่งที่เราคาดหวัง

ตัวอย่างที่ 3) นอกจากนี้ที่นี่เรามีการก่อสร้างและการเคลื่อนไหวบางอย่างตามที่คาดไว้ ด้วยการใช้ stl ของฉันเวกเตอร์จะโตขึ้นตามตัวคูณ 2 ทุกครั้ง ดังนั้นเราเห็นโครงสร้างแรกอีกอันหนึ่งและเนื่องจากเวกเตอร์ปรับขนาดจาก 1 เป็น 2 เราจึงเห็นการย้ายองค์ประกอบแรก ในขณะที่เพิ่ม 3 เราจะเห็นการปรับขนาดจาก 2 เป็น 4 ซึ่งต้องการการย้ายองค์ประกอบสองรายการแรก ทั้งหมดตามที่คาดไว้!

ตัวอย่างที่ 4) ตอนนี้เราจองพื้นที่และเติมในภายหลัง ตอนนี้เราไม่มีการคัดลอกและไม่มีการเคลื่อนไหวอีกต่อไป!

ในทุกกรณีเราไม่เห็นการย้ายหรือการคัดลอกโดยการคืนเวกเตอร์กลับไปยังผู้โทรเลย! (N) RVO กำลังเกิดขึ้นและไม่จำเป็นต้องดำเนินการใด ๆ เพิ่มเติมในขั้นตอนนี้!

กลับไปที่คำถามของคุณ:

"วิธีค้นหาการคัดลอกปลอมแบบ C ++"

ดังที่เห็นด้านบนคุณอาจแนะนำคลาสพร็อกซีในระหว่างนั้นเพื่อการดีบัก

การทำให้สำเนา -ctor เป็นส่วนตัวอาจไม่ทำงานในหลายกรณีเนื่องจากคุณอาจมีสำเนาที่ต้องการและมีบางส่วนที่ซ่อนอยู่ ข้างต้นเฉพาะโค้ดสำหรับ 4 ตัวอย่างเท่านั้นที่จะทำงานร่วมกับ ctor แบบส่วนตัว! และฉันไม่สามารถตอบคำถามได้หากตัวอย่างที่ 4 เป็นวิธีที่เร็วที่สุดในขณะที่เราเติมเต็มความสงบด้วยความสงบสุข

ขออภัยที่ฉันไม่สามารถเสนอทางออกทั่วไปสำหรับการค้นหาสำเนา "ไม่พึงประสงค์" ที่นี่ แม้ว่าคุณจะขุดรหัสของคุณสำหรับการโทรmemcpyคุณจะไม่พบทั้งหมดตามที่memcpyจะได้รับการปรับให้เหมาะสมและคุณจะเห็นคำแนะนำแอสเซมเบลอร์แอสเซมเบลอร์ทำงานโดยตรงโดยไม่ต้องเรียกใช้memcpyฟังก์ชันห้องสมุดของคุณ

คำแนะนำของฉันไม่ได้มุ่งเน้นไปที่ปัญหาเล็ก ๆ น้อย ๆ หากคุณมีปัญหาด้านประสิทธิภาพการทำงานให้ใช้เครื่องมือสร้างโปรไฟล์และการวัด มีนักฆ่าประสิทธิภาพที่มีศักยภาพมากมายที่ใช้เวลามากกับการmemcpyใช้งานแบบปลอมดูเหมือนจะไม่เป็นความคิดที่คุ้มค่า


คำถามของฉันคือวิชาเคมี ใช่มีหลายวิธีที่จะมีรหัสช้าและนี่ไม่ใช่ปัญหาทันทีสำหรับฉัน อย่างไรก็ตามเราสามารถค้นหาการดำเนินการmemcpy ได้โดยใช้ตัวรวบรวมคอมไพเลอร์ ดังนั้นจึงมีวิธีแน่นอน แต่เป็นไปได้สำหรับโปรแกรมขนาดเล็กเท่านั้น จุดของฉันคือมีความสนใจของรหัสที่จะหาคำแนะนำเกี่ยวกับวิธีการปรับปรุงรหัส มีตัววิเคราะห์รหัสที่ค้นหาข้อบกพร่องและการรั่วไหลของหน่วยความจำทำไมจึงไม่มีปัญหาดังกล่าว
Mathieu Dutour Sikiric

"รหัสที่จะหาคำแนะนำเกี่ยวกับวิธีปรับปรุงรหัส" ที่ได้ทำไปแล้วและนำไปใช้ในคอมไพเลอร์เอง (N) การเพิ่มประสิทธิภาพ RVO เป็นเพียงตัวอย่างเดียวและทำงานได้อย่างสมบูรณ์แบบตามที่แสดงข้างต้น การจับ memcpy ไม่ได้ช่วยในขณะที่คุณกำลังค้นหา "memcpy ที่ไม่ต้องการ" "มีตัววิเคราะห์รหัสที่ค้นหาข้อบกพร่องและการรั่วไหลของหน่วยความจำทำไมจึงไม่มีปัญหาดังกล่าว" อาจไม่ใช่ปัญหา (ทั่วไป) และเครื่องมือทั่วไปอื่น ๆ เพื่อค้นหาปัญหา "ความเร็ว" ก็มีอยู่แล้วเช่นกัน: profiler! ความรู้สึกส่วนตัวของฉันคือคุณกำลังมองหาสิ่งที่เป็นวิชาการซึ่งไม่ใช่ปัญหาในซอฟต์แวร์จริง ๆ ในปัจจุบัน
Klaus

1

ฉันรู้ว่ามีการดำเนินการคัดลอกเกิดขึ้นเพราะฉันใช้คอมไพเลอร์ explorer และแสดงการเรียกไปยัง memcpy

คุณใส่แอปพลิเคชันที่สมบูรณ์ของคุณลงในตัวรวบรวมคอมไพเลอร์แล้วหรือยังและเปิดใช้งานการปรับให้เหมาะสมที่สุด ถ้าไม่เช่นนั้นสิ่งที่คุณเห็นในตัวรวบรวมคอมไพเลอร์อาจเป็นหรือไม่เป็นสิ่งที่เกิดขึ้นกับแอปพลิเคชันของคุณ

ประเด็นหนึ่งที่มีรหัสที่คุณโพสต์ให้คุณสร้างและคัดลอกลงในอินสแตนซ์ของstd::vector dataมันจะเป็นการดีกว่าที่จะเริ่มต้น dataกับเวกเตอร์:

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

นอกจากนี้หากคุณเพียงแค่ให้คำจำกัดความของคอมไพเลอร์ explorer dataและget_vector()และไม่มีอะไรอื่นมันต้องคาดหวังที่เลวร้ายยิ่งขึ้น หากคุณให้รหัสแหล่งที่มาที่ใช้ จริงให้get_vector()ดูที่แอสเซมบลีที่สร้างขึ้นสำหรับรหัสแหล่งที่มานั้น ดูตัวอย่างนี้สำหรับสิ่งที่การแก้ไขข้างต้นรวมถึงการใช้งานจริงและการเพิ่มประสิทธิภาพคอมไพเลอร์สามารถทำให้คอมไพเลอร์ผลิต


ฉันใส่คอมพิวเตอร์ explorer โค้ดข้างต้น (ที่มีmemcpy ) มิฉะนั้นคำถามจะไม่สมเหตุสมผล ที่กล่าวว่าคำตอบของคุณเป็นเลิศในการแสดงวิธีที่แตกต่างในการผลิตรหัสที่ดีกว่า คุณจัดเตรียมสองวิธี: การใช้สแตติกและการวางคอนสตรัคเตอร์ในเอาต์พุตโดยตรง ดังนั้นวิธีการเหล่านั้นอาจได้รับการแนะนำโดยเครื่องวิเคราะห์รหัส
Mathieu Dutour Sikiric
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.