std :: function vs template


161

ขอบคุณ C ++ 11 ที่เราได้รับการstd::functionห่อหุ้มของ functor น่าเสียดายที่ฉันได้ยิน แต่เรื่องเลวร้ายเกี่ยวกับการเพิ่มสิ่งใหม่เหล่านี้เท่านั้น ความนิยมมากที่สุดคือพวกเขาช้าอย่างน่ากลัว ฉันทดสอบมันและพวกเขาดูดอย่างแท้จริงเมื่อเปรียบเทียบกับแม่แบบ

#include <iostream>
#include <functional>
#include <string>
#include <chrono>

template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }

float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        calc1([](float arg){ return arg * 0.5f; });
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    return 0;
}

111 มิลลิวินาทีกับ 1241 มิลลิวินาที ฉันคิดว่านี่เป็นเพราะเทมเพลตสามารถอินไลน์ได้ดีfunction s ครอบคลุม internals ผ่านสายเสมือน

แม่แบบเห็นได้ชัดว่ามีปัญหาในขณะที่ฉันเห็นพวกเขา:

  • พวกเขาจะต้องให้เป็นส่วนหัวซึ่งไม่ใช่สิ่งที่คุณอาจไม่ต้องการเมื่อปล่อยห้องสมุดของคุณเป็นรหัสปิด
  • พวกเขาอาจทำให้การรวบรวมเวลานานขึ้นเว้นแต่ extern templateจะมีการแนะนำนโยบายที่คล้ายกัน
  • ไม่มีวิธีที่สะอาด (อย่างน้อยฉันรู้จัก) ในการแสดงความต้องการ (แนวคิด, ใคร?) ของเทมเพลต, แสดงความคิดเห็นที่อธิบายถึงสิ่งที่ประเภทของนักแสดงคาดหวัง

ฉันสามารถสมมติว่าfunctions สามารถใช้เป็นมาตรฐานจริง ๆของการส่งผ่าน functors และในสถานที่ที่ควรใช้เทมเพลตประสิทธิภาพสูงหรือไม่


แก้ไข:

คอมไพเลอร์ของฉันคือ Visual Studio 2012 ที่ไม่มี CTP


16
ใช้std::functionถ้าหากคุณต้องการคอลเลกชันที่แตกต่างกันของวัตถุที่เรียกได้เท่านั้น (เช่นไม่มีข้อมูลการแบ่งแยกที่สามารถใช้งานได้ในขณะใช้งาน)
Kerrek SB

30
คุณกำลังเปรียบเทียบสิ่งที่ผิด เทมเพลตถูกใช้ในทั้งสองกรณี - ไม่ใช่ " std::functionหรือเทมเพลต" ผมคิดว่านี่เป็นปัญหาเป็นเพียงการตัดแลมบ์ดาในstd::functionVS std::functionไม่ตัดแลมบ์ดาใน ในขณะที่คำถามของคุณเหมือนกับถามว่า "ฉันควรเลือกแอปเปิลหรือชามไหม"
การแข่งขัน Lightness ใน Orbit

7
ไม่ว่าจะเป็น 1ns หรือ 10ns ทั้งคู่ก็ไม่มีอะไร
ipc

23
@ipc: 1000% ไม่มีอะไรเลย เมื่อ OP ระบุตัวคุณจะเริ่มสนใจเมื่อมีความยืดหยุ่นในการใช้งานไม่ว่าจะด้วยวิธีใดก็ตาม
การแข่งขัน Lightness ใน Orbit

18
@ipc ช้ากว่าถึง 10 เท่าซึ่งใหญ่มาก ความเร็วจะต้องมีการเปรียบเทียบกับพื้นฐาน; มันหลอกลวงที่จะคิดว่ามันไม่สำคัญเพียงเพราะมันเป็นนาโนวินาที
พอล Manta

คำตอบ:


170

โดยทั่วไปหากคุณกำลังเผชิญกับสถานการณ์การออกแบบที่ให้คุณเลือกใช้เท็มเพลต ฉันเน้นคำนั้นออกแบบเพราะฉันคิดว่าสิ่งที่คุณต้องมุ่งเน้นคือความแตกต่างระหว่างกรณีการใช้งานของstd::functionและแม่แบบซึ่งแตกต่างกันมาก

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

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

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

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

std::functionและstd::bindยังเสนอสำนวนธรรมชาติสำหรับเปิดใช้งานการเขียนโปรแกรมการทำงานใน C ++ ที่ฟังก์ชั่นจะถือว่าเป็นวัตถุและได้รับการ curried ตามธรรมชาติและรวมกันเพื่อสร้างฟังก์ชั่นอื่น ๆ แม้ว่าการรวมกันชนิดนี้สามารถทำได้ด้วยเทมเพลตด้วยเช่นกัน แต่สถานการณ์การออกแบบที่คล้ายกันมักมาพร้อมกับกรณีการใช้งานที่จำเป็นต้องกำหนดประเภทของออบเจ็กต์ที่รวมได้ในเวลาทำงาน

ในที่สุดก็มีสถานการณ์อื่น ๆ ที่std::functionหลีกเลี่ยงไม่ได้เช่นถ้าคุณต้องการเขียนlambdas ซ้ำ ; อย่างไรก็ตามข้อ จำกัด เหล่านี้ถูกกำหนดโดยข้อ จำกัด ทางเทคโนโลยีมากกว่าโดยความแตกต่างทางแนวคิดที่ฉันเชื่อ

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


23
ฉันคิดว่า "นี่เป็นกรณีปกติเมื่อคุณมีคอลเลกชันของการเรียกกลับที่อาจแตกต่างกัน แต่คุณต้องเรียกใช้อย่างสม่ำเสมอ" เป็นบิตที่สำคัญ กฎง่ายๆของฉันคือ: "ชอบstd::functionที่เก็บข้อมูลและเทมเพลตFunบนอินเทอร์เฟซ"
R. Martinho Fernandes

2
หมายเหตุ: เทคนิคการซ่อนประเภทคอนกรีตเรียกว่าการลบประเภท (เพื่อไม่ให้สับสนกับการลบประเภทในภาษาที่จัดการ) มันมักจะนำมาใช้ในแง่ของความหลากหลายแบบไดนามิก แต่มีประสิทธิภาพมากขึ้น (เช่นการunique_ptr<void>เรียก destructors ที่เหมาะสมแม้สำหรับประเภทที่ไม่มี destructors เสมือน)
ecatmur

2
@ecatmur: ฉันเห็นด้วยกับสารถึงแม้ว่าเราจะไม่ได้ทำเครื่องหมายเล็กน้อยเกี่ยวกับคำศัพท์ ความแตกต่างแบบไดนามิกหมายถึงฉัน "สมมติว่ารูปแบบที่แตกต่างกันในเวลาทำงาน" เมื่อเทียบกับความแตกต่างแบบคงที่ซึ่งฉันตีความว่า "สมมติว่ารูปแบบที่แตกต่างกันในการรวบรวมเวลา"; ไม่สามารถทำได้หลังผ่านเทมเพลต สำหรับฉันแล้วการลบประเภทคือการออกแบบที่ชาญฉลาดซึ่งเป็นเงื่อนไขเบื้องต้นสำหรับความสามารถในการทำให้เกิดความแตกต่างหลากหลายแบบไดนามิก: คุณต้องมีส่วนต่อประสานที่เหมือนกันบางอย่างเพื่อโต้ตอบกับวัตถุประเภทต่าง ๆ และการลบประเภทเป็นวิธีนามธรรม ข้อมูลเฉพาะ
Andy Prowl

2
@ecatmur: ดังนั้นในความแตกต่างแบบไดนามิกเป็นรูปแบบความคิดในขณะที่การลบประเภทเป็นเทคนิคที่ช่วยให้ตระหนักถึงมัน
Andy Prowl

2
@Downvoter: ฉันอยากรู้อยากเห็นสิ่งที่คุณพบว่าผิดในคำตอบนี้
Andy Prowl

89

Andy Prowl ได้ครอบคลุมปัญหาการออกแบบอย่างดี นี้เป็นของหลักสูตรที่สำคัญมาก std::functionแต่ผมเชื่อว่าความกังวลที่คำถามเดิมปัญหาประสิทธิภาพการทำงานที่เกี่ยวข้องกับ

ก่อนอื่นคำพูดอย่างรวดเร็วเกี่ยวกับเทคนิคการวัด: 11ms ที่ได้รับสำหรับcalc1ไม่มีความหมายเลย อันที่จริงแล้วเมื่อมองไปที่แอสเซมบลีที่สร้างขึ้น (หรือการดีบักรหัสแอสเซมบลี) เราจะเห็นว่าเครื่องมือเพิ่มประสิทธิภาพของ VS2012 นั้นฉลาดพอที่จะตระหนักได้ว่าผลลัพธ์ของการโทรcalc1นั้นไม่ขึ้นอยู่กับการวนซ้ำ

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

นอกจากนี้ยังตระหนักดีว่าการโทรcalc1ไม่มีผลกระทบที่มองเห็นได้และวางสายทั้งหมด ดังนั้น 111ms เป็นเวลาที่วนรอบที่ว่างเปล่าจะทำงาน (ฉันประหลาดใจที่เครื่องมือเพิ่มประสิทธิภาพรักษาลูปได้) ดังนั้นควรระมัดระวังในการวัดเวลาด้วยลูป มันไม่ง่ายอย่างที่คิด

เครื่องมือเพิ่มประสิทธิภาพมีปัญหาในการเข้าใจมากกว่าstd::functionและไม่ย้ายการโทรออกจากลูป ดังนั้น 1241ms calc2เป็นวัดที่ยุติธรรมสำหรับ

ขอให้สังเกตว่าstd::functionสามารถเก็บวัตถุชนิดที่เรียกได้ ดังนั้นมันจะต้องใช้เวทย์มนตร์ลบประเภทสำหรับการจัดเก็บ โดยทั่วไปนี่หมายถึงการจัดสรรหน่วยความจำแบบไดนามิก (โดยค่าเริ่มต้นผ่านการเรียกไปยังnew) เป็นที่ทราบกันดีว่านี่เป็นการดำเนินการที่ค่อนข้างมีราคาแพง

มาตรฐาน (20.8.11.2.1 / 5) การใช้งาน encorages เพื่อหลีกเลี่ยงการจัดสรรหน่วยความจำแบบไดนามิกสำหรับวัตถุขนาดเล็กซึ่งโชคดีที่ VS2012 ทำ (โดยเฉพาะสำหรับรหัสต้นฉบับ)

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

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

สำหรับรุ่นนี้เวลาประมาณ 16000 มิลลิวินาที (เมื่อเทียบกับรหัสต้นฉบับ 1241 มิลลิวินาที)

std::functionสุดท้ายแจ้งให้ทราบว่าอายุการใช้งานของแลมบ์ดาที่ล้อมรอบที่ของ ในกรณีนี้แทนที่จะเก็บสำเนาแลมบ์ดาstd::functionสามารถเก็บ "อ้างอิง" ไว้ได้ โดย "อ้างอิง" ผมหมายถึงการstd::reference_wrapperที่เป็นได้อย่างง่ายดายโดยการสร้างฟังก์ชั่นและstd::ref std::crefแม่นยำยิ่งขึ้นโดยใช้:

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

เวลาจะลดลงประมาณ 1860 มิลลิวินาที

ฉันเขียนเกี่ยวกับที่ในขณะที่ผ่านมา:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

ดังที่ฉันได้กล่าวไว้ในบทความข้อโต้แย้งไม่ได้ใช้กับ VS2010 มากนักเนื่องจากการสนับสนุนที่แย่ของ C ++ 11 ในช่วงเวลาของการเขียนมีเพียงรุ่นเบต้าของ VS2012 เท่านั้นที่มีอยู่ แต่การสนับสนุนสำหรับ C ++ 11 นั้นดีพอสำหรับเรื่องนี้แล้ว


ฉันพบว่าสิ่งนี้น่าสนใจจริง ๆ แล้วต้องการพิสูจน์ความเร็วโค้ดโดยใช้ตัวอย่างของเล่นที่คอมไพเลอร์ได้รับการปรับให้เหมาะสมเพราะมันไม่มีผลข้างเคียงใด ๆ ฉันจะบอกว่าไม่มีใครสามารถเดิมพันในการวัดประเภทนี้ได้โดยไม่ต้องใช้รหัสจริง / รหัสการผลิต
Ghita

@ Ghita: ในตัวอย่างนี้เพื่อป้องกันไม่ให้โค้ดถูกปรับให้เหมาะสมcalc1อาจใช้floatอาร์กิวเมนต์ที่เป็นผลมาจากการทำซ้ำครั้งก่อน x = calc1(x, [](float arg){ return arg * 0.5f; });สิ่งที่ชอบ นอกจากนี้เราต้องให้แน่ใจว่าการใช้งานcalc1 xแต่มันยังไม่เพียงพอ เราต้องสร้างผลข้างเคียง ตัวอย่างเช่นหลังจากการวัดการพิมพ์xบนหน้าจอ แม้ว่าฉันยอมรับว่าการใช้รหัสของเล่นสำหรับการวัด timimg ไม่สามารถบ่งชี้ได้อย่างสมบูรณ์แบบว่าเกิดอะไรขึ้นกับรหัสจริง / การผลิต
Cassio Neri

ดูเหมือนว่าสำหรับฉันแล้วมาตรฐานนั้นสร้างวัตถุ std :: function ภายในลูปและเรียก calc2 ในลูป ไม่ว่าคอมไพเลอร์อาจจะเหมาะสมหรือไม่ปรับ (และคอนสตรัคเตอร์นั้นง่ายเหมือนการจัดเก็บ vptr) ฉันจะสนใจในกรณีที่ฟังก์ชั่นถูกสร้างขึ้นครั้งเดียวและส่งผ่านไปยังฟังก์ชันอื่นที่เรียก มันเป็นวง เช่นค่าใช้จ่ายในการโทรมากกว่าเวลาที่สร้าง (และการเรียกของ 'f' และไม่ใช่ของ calc2) จะสนใจเช่นกันถ้าการเรียก f ในลูป (ใน calc2) แทนที่จะเป็นหนึ่งครั้งจะได้ประโยชน์จากการชักรอกใด ๆ
greggo

คำตอบที่ดี 2 สิ่ง: ตัวอย่างที่ดีของการใช้งานที่ถูกต้องสำหรับstd::reference_wrapper(เพื่อเทมเพลต coerce; ไม่ใช่สำหรับที่จัดเก็บข้อมูลทั่วไป), และมันเป็นเรื่องตลกที่เห็นเครื่องมือเพิ่มประสิทธิภาพของ VS ไม่สามารถละทิ้งลูปว่าง ... ตามที่ฉันสังเกตเห็นด้วยข้อผิดพลาด GCCvolatileนี้
underscore_d

37

ด้วยเสียงดังกราวไม่มีความแตกต่างด้านประสิทธิภาพระหว่างทั้งสอง

การใช้เสียงดังกราว (3.2 ลำต้น 166,872) (-O2 บน Linux) ไบนารีจากทั้งสองกรณีจะเหมือนจริง

- ฉันจะกลับมาดังกราวในตอนท้ายของโพสต์ แต่ก่อนอื่น gcc 4.7.2:

มีความเข้าใจมากมายเกิดขึ้น แต่ฉันต้องการชี้ให้เห็นว่าผลลัพธ์ของการคำนวณของ calc1 และ calc2 นั้นไม่เหมือนกันเนื่องจากอยู่ในรูปแบบอื่น ๆ เปรียบเทียบตัวอย่างเช่นผลรวมของผลลัพธ์ทั้งหมด:

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

ด้วย calc2 ที่จะกลายเป็น

1.71799e+10, time spent 0.14 sec

ในขณะที่กับ calc1 มันจะกลายเป็น

6.6435e+10, time spent 5.772 sec

นั่นคือปัจจัยของความแตกต่างความเร็ว ~ 40 และปัจจัย ~ 4 ในค่า สิ่งแรกคือความแตกต่างที่ยิ่งใหญ่กว่าสิ่งที่ OP โพสต์ (โดยใช้ visual studio) การพิมพ์ค่าจริงๆแล้วจุดจบก็เป็นความคิดที่ดีที่จะป้องกันไม่ให้คอมไพเลอร์ลบโค้ดโดยไม่มีผลลัพธ์ที่มองเห็นได้ (ตามกฎถ้า) Cassio Neri ตอบคำถามนี้ในคำตอบของเขาแล้ว โปรดทราบว่าผลลัพธ์ต่างกันอย่างไร - เราควรระมัดระวังเมื่อเปรียบเทียบปัจจัยความเร็วของรหัสที่ใช้ในการคำนวณที่แตกต่างกัน

นอกจากนี้เพื่อความเป็นธรรมการเปรียบเทียบวิธีต่างๆในการคำนวณ f (3.3) ซ้ำ ๆ อาจไม่น่าสนใจ หากอินพุตคงที่ไม่ควรอยู่ในลูป (เป็นเรื่องง่ายสำหรับเครื่องมือเพิ่มประสิทธิภาพที่จะสังเกตเห็น)

ถ้าฉันเพิ่มอาร์กิวเมนต์ค่าที่ผู้ใช้ระบุให้กับ calc1 และ 2 ตัวคูณความเร็วระหว่าง calc1 และ calc2 จะลดลงเป็น 5 จาก 40! ด้วย visual studio ความแตกต่างนั้นใกล้เคียงกับปัจจัยที่ 2 และเสียงดังกราวไม่มีความแตกต่าง (ดูด้านล่าง)

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

เสียงดังกราว:

เสียงดังกราว (ฉันใช้ 3.2) จริง ๆ แล้วสร้างไบนารีที่เหมือนกันเมื่อฉันพลิกระหว่าง calc1 และ calc2 สำหรับโค้ดตัวอย่าง (โพสต์ด้านล่าง) ด้วยตัวอย่างดั้งเดิมที่โพสต์ในคำถามทั้งคู่ก็เหมือนกัน แต่ไม่ต้องใช้เวลาเลย (ลูปจะถูกลบออกอย่างสมบูรณ์ตามที่อธิบายไว้ข้างต้น) ด้วยตัวอย่างที่แก้ไขของฉันด้วย -O2:

จำนวนวินาทีในการดำเนินการ (ดีที่สุด 3):

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc 4.7.2:    calc1:           1.1 seconds
gcc 4.7.2:    calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

VS2015 (14.0.23.107) calc1:    1.1 seconds 
VS2015 (14.0.23.107) calc2:    1.5 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

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

รหัสทดสอบที่แก้ไขของฉัน:

#include <functional>
#include <chrono>
#include <iostream>

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = high_resolution_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    std::cout << result<< std::endl;
    return 0;
}

ปรับปรุง:

เพิ่ม vs2015 ฉันยังสังเกตเห็นว่ามีการแปลงแบบลอย - สองครั้งใน calc1, calc2 การนำออกจะไม่เปลี่ยนข้อสรุปสำหรับ visual studio (ทั้งคู่เร็วกว่ามาก แต่อัตราส่วนใกล้เคียงกัน)


8
ซึ่งเนื้อหาเพียงแสดงให้เห็นว่ามาตรฐานนั้นผิด IMHO กรณีการใช้งานที่น่าสนใจคือที่ที่รหัสการโทรได้รับฟังก์ชั่นวัตถุจากที่อื่นดังนั้นคอมไพเลอร์ไม่ทราบที่มาของฟังก์ชัน std :: เมื่อรวบรวมการโทร ที่นี่คอมไพเลอร์รู้องค์ประกอบของ std :: function อย่างแท้จริงเมื่อเรียกมันโดยการขยาย calc2 แบบอินไลน์เข้าไปใน main แก้ไขได้อย่างง่ายดายด้วยการทำให้ calc2 'extern' เป็นเดือนกันยายน ไฟล์ต้นฉบับ คุณกำลังเปรียบเทียบแอปเปิ้ลกับส้ม calc2 กำลังทำอะไรบางอย่างที่ calc1 ไม่สามารถทำได้ และการวนรอบอาจอยู่ภายใน calc (การโทรไปยัง f) หลายสาย ไม่ใช่รอบ ๆ ctor ของวัตถุฟังก์ชัน
greggo

1
เมื่อฉันได้รับการรวบรวมที่เหมาะสม สามารถพูดได้ว่าตอนนี้ (a) ctor สำหรับ std :: function call 'new' จริง (b) การเรียกตัวเองนั้นค่อนข้างเอนเมื่อเป้าหมายคือฟังก์ชันที่ตรงกันจริง (c) ในกรณีที่มีการเชื่อมโยงมีชิ้นส่วนของรหัสซึ่งทำการปรับเลือกโดยรหัส ptr ในฟังก์ชั่น obj และที่รับข้อมูล (parms ที่ถูกผูก) จากฟังก์ชัน obj (d) ฟังก์ชัน 'ขอบเขต' อาจ จะถูกแทรกลงในอะแดปเตอร์นั้นหากคอมไพเลอร์สามารถมองเห็นได้
greggo

เพิ่มคำตอบใหม่ด้วยการตั้งค่าที่อธิบายไว้
greggo

3
BTW เกณฑ์มาตรฐานไม่ผิดคำถาม ("std :: function vs template") ใช้ได้เฉพาะในขอบเขตของหน่วยการคอมไพล์เดียวกันเท่านั้น หากคุณย้ายฟังก์ชันไปยังหน่วยอื่นเทมเพลตจะไม่สามารถทำได้อีกต่อไปดังนั้นจึงไม่มีอะไรที่จะเปรียบเทียบได้
rustyx

13

แตกต่างกันไม่เหมือนกัน

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

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

โปรดทราบว่าเดียวกันวัตถุฟังก์ชั่นจะถูกส่งผ่านไปยังสายทั้งสองfun evalมันมีฟังก์ชั่นที่แตกต่างกันสองอย่าง

หากคุณไม่จำเป็นต้องทำอย่างนั้นแล้วคุณควรจะไม่ได้std::functionใช้งาน


2
เพียงแค่ต้องการชี้ให้เห็นว่าเมื่อเสร็จสิ้น 'fun = f2' แล้ววัตถุ 'fun' จะชี้ไปที่ฟังก์ชันที่ซ่อนอยู่ซึ่งแปลง int เป็น double เรียกใช้ f2 และแปลงผลลัพธ์ double กลับเป็น int (ในตัวอย่างจริง , 'f2' สามารถถูก Inlined เข้าไปในฟังก์ชันนั้น) หากคุณกำหนด std :: bind ให้กับความสนุกวัตถุ 'fun' สามารถลงท้ายด้วยค่าที่จะใช้สำหรับพารามิเตอร์ที่ถูกผูกไว้ เพื่อรองรับความยืดหยุ่นนี้การมอบหมายให้ 'สนุก' (หรือผู้เริ่มต้น) สามารถเกี่ยวข้องกับการจัดสรร / ยกเลิกการจัดสรรหน่วยความจำและอาจใช้เวลานานกว่าค่าโทรจริง
greggo

8

คุณมีคำตอบที่ดีอยู่แล้วดังนั้นฉันจะไม่โต้แย้งพวกเขาในการเปรียบเทียบ std :: function กับ template ก็เหมือนกับการเปรียบเทียบฟังก์ชันเสมือนกับฟังก์ชัน คุณไม่ควร "ชอบ" ฟังก์ชั่นเสมือนจริงของฟังก์ชั่น แต่คุณควรใช้ฟังก์ชั่นเสมือนจริงเมื่อมันเหมาะกับปัญหาย้ายการตัดสินใจจากเวลารวบรวมเพื่อเรียกใช้เวลา แนวคิดคือแทนที่จะต้องแก้ปัญหาโดยใช้โซลูชัน bespoke (เช่นตารางกระโดด) คุณใช้สิ่งที่ให้คอมไพเลอร์มีโอกาสที่ดีขึ้นในการปรับให้เหมาะสมสำหรับคุณ นอกจากนี้ยังช่วยโปรแกรมเมอร์อื่น ๆ หากคุณใช้โซลูชันมาตรฐาน


6

คำตอบนี้มีวัตถุประสงค์เพื่อสนับสนุนชุดคำตอบที่มีอยู่สิ่งที่ฉันเชื่อว่าเป็นเกณฑ์มาตรฐานที่มีความหมายมากขึ้นสำหรับต้นทุนรันไทม์ของ std :: function call

ควรรับรู้กลไกของฟังก์ชัน std :: สำหรับสิ่งที่มีให้: เอนทิตี callable ใด ๆ สามารถแปลงเป็นฟังก์ชัน std :: ของลายเซ็นที่เหมาะสม สมมติว่าคุณมีไลบรารีที่เหมาะกับพื้นผิวกับฟังก์ชันที่กำหนดโดย z = f (x, y) คุณสามารถเขียนมันเพื่อยอมรับ a std::function<double(double,double)>และผู้ใช้ไลบรารีสามารถแปลงเอนทิตี callable ใด ๆ ได้อย่างง่ายดาย ไม่ว่าจะเป็นฟังก์ชั่นธรรมดาวิธีการของอินสแตนซ์ของชั้นเรียนหรือแลมบ์ดาหรืออะไรก็ตามที่ได้รับการสนับสนุนโดย std :: bind

แตกต่างจากวิธีการของเทมเพลตการทำงานนี้โดยไม่ต้องคอมไพล์ฟังก์ชันไลบรารีอีกครั้งสำหรับกรณีที่แตกต่างกัน ดังนั้นจึงจำเป็นต้องมีการคอมไพล์โค้ดพิเศษเล็กน้อยสำหรับแต่ละเคสเพิ่มเติม มีความเป็นไปได้ที่จะทำให้เกิดขึ้นเสมอ แต่มันต้องใช้กลไกที่น่าอึดอัดใจและผู้ใช้ไลบรารีอาจต้องสร้างอะแดปเตอร์รอบฟังก์ชันเพื่อให้ทำงานได้ std :: function สร้างสิ่งที่อะแดปเตอร์จำเป็นโดยอัตโนมัติเพื่อให้ได้ทั่วไปอินเทอร์เฟซการเรียกรันไทม์สำหรับทุกกรณีซึ่งเป็นคุณสมบัติใหม่และทรงพลังมาก

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

ฉันทำแบบทดสอบด้านล่างคล้ายกับของ OP แต่การเปลี่ยนแปลงที่สำคัญคือ:

  1. แต่ละเคสวน 1 พันล้านครั้ง แต่วัตถุฟังก์ชัน std :: ถูกสร้างเพียงครั้งเดียว ฉันพบโดยดูที่รหัสผลลัพธ์ที่เรียกว่า 'ตัวดำเนินการใหม่' เมื่อสร้าง std :: function calls จริง (อาจไม่ใช่เมื่อพวกมันถูกปรับให้เหมาะสม)
  2. การทดสอบแบ่งออกเป็นสองไฟล์เพื่อป้องกันการเพิ่มประสิทธิภาพที่ไม่พึงประสงค์
  3. กรณีของฉันคือ: (a) ฟังก์ชั่น inlined (b) ฟังก์ชั่นผ่านตัวชี้ฟังก์ชั่นธรรมดา (c) ฟังก์ชั่นเป็นฟังก์ชั่นที่เข้ากันได้ห่อเป็น std :: function (d) ฟังก์ชั่น ผูกห่อเป็นมาตรฐาน :: ฟังก์ชั่น

ผลลัพธ์ที่ฉันได้รับคือ:

  • กรณี (ก) (อินไลน์) 1.3 nsec

  • กรณีอื่น ๆ ทั้งหมด: 3.3 วินาที

กรณี (d) มีแนวโน้มที่จะช้าลงเล็กน้อย แต่ความแตกต่าง (ประมาณ 0.05 nsec) ถูกดูดซับไว้ในเสียง

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

เมื่อฉันรันโค้ดของ johan-lundberg บนเครื่องเดียวกันฉันเห็นประมาณ 39 nsec ต่อลูป แต่มีจำนวนมากในลูปที่นั่นรวมถึงตัวสร้างจริงและ destructor ของฟังก์ชัน std :: ซึ่งอาจค่อนข้างสูง เพราะมันเกี่ยวข้องกับการใหม่และลบ

-O2 gcc 4.8.1, ถึง x86_64 เป้าหมาย (core i5)

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

----- ไฟล์ต้นฉบับแรก --------------

#include <functional>


// simple funct
float func_half( float x ) { return x * 0.5; }

// func we can bind
float mul_by( float x, float scale ) { return x * scale; }

//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with inline function
float test_inline(  int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func_half(x);
    }
    return y;
}

----- ไฟล์แหล่งที่สอง -------------

#include <iostream>
#include <functional>
#include <chrono>

extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline(  int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );

int main() {
    using namespace std::chrono;


    for(int icase = 0; icase < 4; icase ++ ){
        const auto tp1 = system_clock::now();

        float result;
        switch( icase ){
         case 0:
            result = test_inline( 1e9);
            break;
         case 1:
            result = test_funcptr( func_half, 1e9);
            break;
         case 2:
            result = test_stdfunc( func_half, 1e9);
            break;
         case 3:
            result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
            break;
        }
        const auto tp2 = high_resolution_clock::now();

        const auto d = duration_cast<milliseconds>(tp2 - tp1);  
        std::cout << d.count() << std::endl;
        std::cout << result<< std::endl;
    }
    return 0;
}

สำหรับผู้ที่สนใจนี่คืออะแดปเตอร์คอมไพเลอร์ที่สร้างขึ้นเพื่อให้ 'mul_by' ดูเหมือนลอย (float) - นี่คือ 'เรียกว่า' เมื่อฟังก์ชั่นที่สร้างขึ้นเป็นผูก (mul_by, _1,0.5) เรียกว่า:

movq    (%rdi), %rax                ; get the std::func data
movsd   8(%rax), %xmm1              ; get the bound value (0.5)
movq    (%rax), %rdx                ; get the function to call (mul_by)
cvtpd2ps    %xmm1, %xmm1        ; convert 0.5 to 0.5f
jmp *%rdx                       ; jump to the func

(ดังนั้นมันอาจเร็วกว่านี้สักหน่อยถ้าฉันเขียน 0.5f ลงไปในการผูก ... ) โปรดทราบว่าพารามิเตอร์ 'x' มาถึงใน% xmm0 และเพิ่งจะอยู่ที่นั่น

นี่คือรหัสในพื้นที่ที่สร้างฟังก์ชันก่อนเรียก test_stdfunc - เรียกใช้ผ่าน c ++ filt:

movl    $16, %edi
movq    $0, 32(%rsp)
call    operator new(unsigned long)      ; get 16 bytes for std::function
movsd   .LC0(%rip), %xmm1                ; get 0.5
leaq    16(%rsp), %rdi                   ; (1st parm to test_stdfunc) 
movq    mul_by(float, float), (%rax)     ; store &mul_by  in std::function
movl    $1000000000, %esi                ; (2nd parm to test_stdfunc)
movsd   %xmm1, 8(%rax)                   ; store 0.5 in std::function
movq    %rax, 16(%rsp)                   ; save ptr to allocated mem

   ;; the next two ops store pointers to generated code related to the std::function.
   ;; the first one points to the adaptor I showed above.

movq    std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq    std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)


call    test_stdfunc(std::function<float (float)> const&, int)

1
ด้วยเสียงดังกราว 3.4.1 x64 ผลลัพธ์คือ: (a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0
rustyx

4

ฉันพบว่าผลลัพธ์ของคุณน่าสนใจมากฉันเลยขุดนิดหน่อยเพื่อทำความเข้าใจว่าเกิดอะไรขึ้น ก่อนอื่นเป็นจำนวนมากได้กล่าวโดยไม่ต้องมีผลลัพธ์ของการคำนวณผลสถานะของโปรแกรมคอมไพเลอร์จะเพียงเพิ่มประสิทธิภาพนี้ออกไป ประการที่สองมีค่าคงที่ 3.3 ที่กำหนดไว้เป็นอาวุธในการติดต่อกลับฉันสงสัยว่าจะมีการเพิ่มประสิทธิภาพอื่น ๆ เกิดขึ้น โดยที่ในใจฉันเปลี่ยนรหัสมาตรฐานของคุณเล็กน้อย

template <typename F>
float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

รับการเปลี่ยนแปลงรหัสนี้ฉันรวบรวมกับ gcc 4.8 -O3 และมีเวลา 330ms สำหรับ calc1 และ 2702 สำหรับ calc2 ดังนั้นการใช้เท็มเพลตก็เร็วขึ้น 8 เท่าตัวเลขนี้ดูน่าสงสัยสำหรับฉันความเร็วของพลัง 8 มักจะบ่งบอกว่าคอมไพเลอร์ได้เวกเตอร์บางอย่าง เมื่อฉันดูโค้ดที่สร้างขึ้นสำหรับเทมเพลตเวอร์ชันมันจะถูกจัดรูปแบบให้ชัดเจน

.L34:
cvtsi2ss        %edx, %xmm0
addl    $1, %edx
movaps  %xmm3, %xmm5
mulss   %xmm4, %xmm0
addss   %xmm1, %xmm0
subss   %xmm0, %xmm5
movaps  %xmm5, %xmm0
addss   %xmm1, %xmm0
cvtsi2sd        %edx, %xmm1
ucomisd %xmm1, %xmm2
ja      .L37
movss   %xmm0, 16(%rsp)

โดยที่เวอร์ชัน std :: function ไม่ถูกต้อง สิ่งนี้สมเหตุสมผลสำหรับฉันเนื่องจากมีเทมเพลตคอมไพเลอร์ก็รู้ดีว่าฟังก์ชั่นจะไม่เปลี่ยนแปลงตลอดทั้งลูป แต่ด้วยฟังก์ชัน std :: ที่ถูกส่งผ่านเข้าไปจะสามารถเปลี่ยนแปลงได้ดังนั้นจึงไม่สามารถแปลงเวกเตอร์ได้

นี่ทำให้ฉันลองอย่างอื่นเพื่อดูว่าฉันจะได้คอมไพเลอร์เพื่อทำการปรับให้เหมาะสมเดียวกันกับ std :: function version หรือไม่ แทนที่จะส่งผ่านฟังก์ชันฉันสร้าง std :: function เป็น global var และเรียกสิ่งนี้

float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };

int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

ด้วยเวอร์ชันนี้เราจะเห็นว่าคอมไพเลอร์ได้ทำการแปลงโค้ดในลักษณะเดียวกันแล้วและฉันก็จะได้ผลลัพธ์ที่เป็นมาตรฐานเดียวกัน

  • แม่แบบ: 330 มิลลิวินาที
  • std :: function: 2702ms
  • global std :: function: 330ms

ดังนั้นข้อสรุปของฉันคือความเร็วที่แท้จริงของ std :: function เทียบกับ functor เทมเพลตก็เหมือนกัน อย่างไรก็ตามมันทำให้งานของเครื่องมือเพิ่มประสิทธิภาพเป็นเรื่องยากมากขึ้น


1
จุดทั้งหมดคือการส่ง functor เป็นพารามิเตอร์ calc3กรณีของคุณไม่มีเหตุผล calc3 ตอนนี้ hardcoded เพื่อโทร f2 แน่นอนว่าสามารถปรับให้เหมาะสม
rustyx

จริง ๆ นี่คือสิ่งที่ฉันพยายามแสดง Calc3 นั้นเทียบเท่ากับเทมเพลตและในสถานการณ์นั้นเวลาในการคอมไพล์จะมีประสิทธิภาพเหมือนกับเทมเพลต
Joshua Ritterman
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.