ทำไมความผันผวนจึงมีอยู่


222

อะไรvolatileคำหลักทำอย่างไร ใน C ++ มันแก้ปัญหาอะไรได้บ้าง?

ในกรณีของฉันฉันไม่เคยต้องการมันอย่างรู้เท่าทัน


นี่คือการสนทนาที่น่าสนใจเกี่ยวกับการเปลี่ยนแปลงเกี่ยวกับรูปแบบซิงเกิล: aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
chessguy

3
มีเทคนิคที่น่าสนใจที่ทำให้คอมไพเลอร์ของคุณตรวจสอบสภาพการแข่งขันไปได้ว่าอาศัยอย่างหนักในคำระเหยคือคุณสามารถอ่านเกี่ยวกับเรื่องที่http://www.ddj.com/cpp/184403766
Neno Ganchev

นี่เป็นแหล่งข้อมูลที่ดีพร้อมตัวอย่างเมื่อvolatileสามารถใช้งานได้อย่างมีประสิทธิภาพใส่กันในแง่คนธรรมดาสวย Link: สิ่งพิมพ์.
gbdirect.co.uk/c_book/chapter8/…

ฉันใช้มันเพื่อล็อครหัสฟรี / การตรวจสอบการล็อคสองครั้ง
paulm

สำหรับฉันvolatileมีประโยชน์มากกว่าfriendคำหลัก
acegs

คำตอบ:


268

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

ฉันเคยทำงานกับ RAM แบบดูอัลพอร์ตในระบบมัลติโพรเซสเซอร์เป็นเส้นตรง C เราใช้ฮาร์ดแวร์ที่จัดการค่า 16 บิตเป็นเซมาฟอร์เพื่อรู้ว่าเมื่อไรที่คนอื่นทำเสร็จ เป็นหลักเราทำสิ่งนี้:

void waitForSemaphore()
{
   volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
   while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}

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


ในกรณีนี้จะเกิดอะไรขึ้นถ้าuint16_t* volatile semPtrเขียนแทน สิ่งนี้ควรทำเครื่องหมายตัวชี้ว่าระเหย (แทนที่จะเป็นค่าที่ชี้ไปที่) เพื่อตรวจสอบตัวชี้เช่นsemPtr == SOME_ADDRอาจไม่ได้รับการปรับให้เหมาะสม อย่างไรก็ตามนี่ก็หมายถึงค่าของความผันผวนที่ชี้ชัดอีกครั้งเช่นกัน ไม่มี?
Zyl

@ Zyl ไม่มันไม่ ในทางปฏิบัติสิ่งที่คุณแนะนำมีแนวโน้มว่าจะเกิดอะไรขึ้น แต่ในทางทฤษฎีเราสามารถใช้คอมไพเลอร์ที่ปรับการเข้าถึงค่าให้เหมาะสมเพราะมันตัดสินใจว่าไม่มีการเปลี่ยนแปลงค่าเหล่านั้นเลย และถ้าคุณหมายถึงความผันผวนที่จะนำไปใช้กับค่าไม่ใช่พอยน์เตอร์คุณก็จะเมา ไม่น่าแปลกใจอีกต่อไป แต่เป็นการดีกว่าที่จะทำสิ่งที่ถูกต้องดีกว่าการใช้ประโยชน์จากพฤติกรรมที่เกิดขึ้นในวันนี้
iheanyi

1
@Doug T. คำอธิบายที่ดีกว่าคือนี่
machineaddict

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

1
@crossguy ไม่เพียงเพราะคำหลักที่มีความผันผวนปรากฏเพียงครั้งเดียวไม่ได้หมายความว่าทุกอย่างจะกลายเป็นความผันผวน ฉันให้สถานการณ์ที่คอมไพเลอร์ทำในสิ่งที่ถูกต้องและได้ผลลัพธ์ที่ตรงข้ามกับที่โปรแกรมเมอร์คาดหวังไว้อย่างผิด ๆ เช่นเดียวกับ "แจงส่วนใหญ่ที่รบกวนจิตใจ" ไม่ใช่เครื่องหมายของข้อผิดพลาดของคอมไพเลอร์ไม่เป็นกรณีที่นี่
iheanyi

82

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


9
สิ่งนี้ไม่เพียงถูกต้องสำหรับระบบฝังตัว แต่สำหรับการพัฒนาไดรเวอร์อุปกรณ์ทั้งหมด
Mladen Janković

เวลาเท่านั้นที่ฉันเคยต้องการมันบนรถบัส 8bit ISA ที่คุณอ่านอยู่เดียวกันสองครั้ง - คอมไพเลอร์มีข้อบกพร่องและไม่สนใจมัน (ต้น Zortech C ++)
มาร์ติน Beckett

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

69

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

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

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


5
เมื่อคุณคำนวณอนุพันธ์เชิงตัวเลขมันก็มีประโยชน์เช่นกันเพื่อให้แน่ใจว่า x + h - x == h คุณกำหนด hh = x + h - x เป็นระเหยเพื่อให้เดลต้าที่เหมาะสมสามารถคำนวณได้
Alexandre C.

5
+1 ในประสบการณ์ของฉันมีกรณีที่การคำนวณจุดลอยตัวสร้างผลลัพธ์ที่แตกต่างกันใน Debug and Release ดังนั้นการทดสอบหน่วยที่เขียนขึ้นสำหรับการกำหนดค่าหนึ่งล้มเหลวสำหรับอีกรายการหนึ่ง เราแก้ไขได้โดยการประกาศตัวแปรทศนิยมหนึ่งตัวvolatile doubleแทนที่จะเป็นเพียงแค่doubleเพื่อให้แน่ใจว่าถูกตัดทอนจากความแม่นยำของ FPU เป็นความแม่นยำ 64 บิต (RAM) ก่อนดำเนินการคำนวณต่อไป ผลลัพธ์มีความแตกต่างอย่างมีนัยสำคัญเนื่องจากมีการพูดเกินจริงเพิ่มเติมของข้อผิดพลาดจุดลอยตัว
Serge Rogatch

คำจำกัดความของคุณเกี่ยวกับ "สมัยใหม่" นั้นค่อนข้างจะซับซ้อน เฉพาะรหัส x86 แบบ 32 บิตที่หลีกเลี่ยง SSE / SSE2 ได้รับผลกระทบจากสิ่งนี้และมันก็ไม่ได้ "ทันสมัย" แม้แต่เมื่อ 10 ปีที่แล้ว MIPS / ARM / POWER ทั้งหมดมีการลงทะเบียนฮาร์ดแวร์ 64 บิตและ x86 ด้วย SSE2 การใช้งาน C ++ x86-64 ใช้ SSE2 เสมอและคอมไพเลอร์มีตัวเลือกที่g++ -mfpmath=sseจะใช้สำหรับ x86 แบบ 32 บิตเช่นกัน คุณสามารถใช้gcc -ffloat-storeบังคับให้ปัดเศษได้ทุกที่แม้ใช้ x87 หรือคุณสามารถตั้งค่าความแม่นยำ x87 เป็น mantissa 53 บิต: randomascii.wordpress.com/2012/03/21/
Peter Cordes

แต่ยังคงเป็นคำตอบที่ดีสำหรับรหัส x87 ที่ล้าสมัยคุณสามารถใช้volatileบังคับให้ปัดเศษในบางสถานที่โดยไม่สูญเสียผลประโยชน์ในทุกที่
Peter Cordes

1
หรือฉันจะสับสนไม่ถูกต้องกับที่ไม่สอดคล้องกัน?
Chipster

49

จากบทความ"ระเหยเป็นสัญญา"โดยแดน Saks:

(... ) วัตถุที่ระเหยได้ซึ่งเป็นค่าที่อาจเปลี่ยนแปลงได้เอง นั่นคือเมื่อคุณประกาศให้วัตถุมีความผันผวนคุณกำลังบอกคอมไพเลอร์ว่าวัตถุนั้นอาจเปลี่ยนสถานะแม้ว่าจะไม่มีคำสั่งในโปรแกรมปรากฏให้เปลี่ยนก็ตาม

นี่คือลิงค์ไปยังสามบทความของเขาเกี่ยวกับvolatileคำหลัก:


23

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

หากต้องการกล่าวอีกวิธีหนึ่ง volatile จะบอกคอมไพเลอร์ว่าการเข้าถึงตัวแปรนี้จะต้องสอดคล้องกับการดำเนินการอ่าน / เขียนหน่วยความจำกายภาพ

ตัวอย่างเช่นนี่คือวิธีการประกาศ InterlockedIncrement ใน Win32 API:

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

คุณไม่จำเป็นต้องประกาศตัวแปรผันผวนเพื่อให้สามารถใช้ InterlockedIncrement ได้
curiousguy

คำตอบนี้ล้าสมัยแล้วในขณะนี้ที่ C ++ 11 มีให้std::atomic<LONG>เพื่อให้คุณสามารถเขียนรหัส lockless ได้อย่างปลอดภัยมากขึ้นโดยไม่มีปัญหาในการโหลดแท้ / ร้านค้าแท้ที่ได้รับการปรับให้เหมาะสม
Peter Cordes

10

แอปพลิเคชันขนาดใหญ่ที่ฉันใช้ในการทำงานในต้นปี 1990 มีการจัดการข้อยกเว้นแบบอิง C โดยใช้ setjmp และ longjmp คำสำคัญระเหยมีความจำเป็นกับตัวแปรที่มีค่าที่จำเป็นต้องเก็บรักษาไว้ในบล็อกของรหัสที่ทำหน้าที่เป็นประโยค "จับ" เพื่อมิให้ vars เหล่านั้นถูกเก็บไว้ในรีจิสเตอร์และลบออกโดย longjmp


10

ในมาตรฐาน C หนึ่งในสถานที่ที่ใช้volatileคือพร้อมตัวจัดการสัญญาณ ในความเป็นจริงในมาตรฐาน C สิ่งที่คุณสามารถทำได้อย่างปลอดภัยในตัวจัดการสัญญาณคือปรับเปลี่ยนvolatile sig_atomic_tตัวแปรหรือออกอย่างรวดเร็ว แท้จริงแล้ว AFAIK เป็นสถานที่แห่งเดียวในมาตรฐาน C ที่volatileจำเป็นต้องใช้เพื่อหลีกเลี่ยงพฤติกรรมที่ไม่ได้กำหนด

ISO / IEC 9899: 2011 §7.14.1.1 signalฟังก์ชั่น

¶5หากสัญญาณเกิดขึ้นนอกเหนือจากที่เป็นผลมาจากการเรียกabortหรือraiseฟังก์ชั่นพฤติกรรมจะไม่ได้กำหนดถ้าตัวจัดการสัญญาณหมายถึงวัตถุใด ๆ ที่มีระยะเวลาการจัดเก็บแบบคงที่หรือด้ายที่ไม่ได้ล็อคอะตอมวัตถุอื่นที่ไม่ใช่ล็อค ไปยังวัตถุที่ประกาศว่าเป็นvolatile sig_atomic_t, หรือตัวจัดการสัญญาณเรียกใช้ฟังก์ชันใด ๆ ในไลบรารีมาตรฐานอื่นนอกเหนือจากabortฟังก์ชัน, _Exitฟังก์ชั่น, quick_exitฟังก์ชั่นหรือsignalฟังก์ชั่นที่มีอาร์กิวเมนต์แรกเท่ากับจำนวนสัญญาณที่สอดคล้องกับสัญญาณที่ก่อให้เกิดการร้องขอ ผู้ดำเนินการ นอกจากนี้หากการเรียกใช้signalฟังก์ชันส่งผลให้ SIG_ERR ส่งคืนค่าของerrnoไม่แน่นอน 252)

252)หากสัญญาณใด ๆ ถูกสร้างขึ้นโดยตัวจัดการสัญญาณแบบอะซิงโครนัสพฤติกรรมจะไม่ได้กำหนด

นั่นหมายความว่าใน Standard C คุณสามารถเขียน:

static volatile sig_atomic_t sig_num = 0;

static void sig_handler(int signum)
{
    signal(signum, sig_handler);
    sig_num = signum;
}

และไม่มาก

POSIX มีความผ่อนปรนมากขึ้นเกี่ยวกับสิ่งที่คุณสามารถทำได้ในตัวจัดการสัญญาณ แต่ยังมีข้อ จำกัด (และหนึ่งในข้อ จำกัด คือว่าไลบรารี I / O มาตรฐาน - printf()et al - ไม่สามารถใช้ได้อย่างปลอดภัย)


7

การพัฒนาสำหรับการฝังตัวฉันมีการวนรอบที่ตรวจสอบตัวแปรที่สามารถเปลี่ยนแปลงได้ในตัวจัดการขัดจังหวะ หากไม่มี "volatile" loop จะกลายเป็น noop เท่าที่คอมไพเลอร์สามารถบอกได้ว่าตัวแปรจะไม่เปลี่ยนแปลงดังนั้นจึงปรับการตรวจสอบให้เหมาะสม

สิ่งเดียวกันจะนำไปใช้กับตัวแปรที่อาจมีการเปลี่ยนแปลงในเธรดที่แตกต่างกันในสภาพแวดล้อมแบบดั้งเดิมมากขึ้น แต่ที่นั่นเรามักจะเรียกการซิงโครไนซ์ดังนั้นคอมไพเลอร์ไม่ได้ฟรีด้วยการเพิ่มประสิทธิภาพ


7

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


7

นอกเหนือจากการใช้ตามที่ตั้งใจไว้แล้วสารระเหยจะใช้ใน (เทมเพลต) การเปรียบเทียบโปรแกรม สามารถใช้เพื่อป้องกันการบรรทุกเกินพิกัดโดยไม่ตั้งใจเนื่องจากแอตทริบิวต์การระเหย (เช่น const) มีส่วนร่วมในการแก้ปัญหาการโอเวอร์โหลด

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

สิ่งนี้ถูกกฎหมาย โอเวอร์โหลดทั้งสองสามารถเรียกได้และเกือบจะเหมือนกัน โยนในvolatileการโอเวอร์โหลดเป็นกฎหมายที่เรารู้ว่าบาร์จะไม่ผ่านไม่ระเหยTอยู่แล้ว volatileรุ่นเป็นอย่างเคร่งครัดแย่ลง แต่ไม่เคยได้รับการแต่งตั้งในความละเอียดเกินถ้าไม่ระเหยfสามารถใช้ได้

โปรดทราบว่ารหัสไม่เคยขึ้นอยู่กับvolatileการเข้าถึงหน่วยความจำ


คุณช่วยอธิบายเรื่องนี้ด้วยตัวอย่างได้ไหม? มันช่วยให้ฉันเข้าใจดีขึ้นจริงๆ ขอบคุณ!
batbrat

" การร่ายในโอเวอร์โหลดระเหยง่าย " การร่ายเป็นการแปลงที่ชัดเจน มันเป็นโครงสร้างของ SYNTAX หลายคนทำให้เกิดความสับสน (แม้แต่ผู้เขียนมาตรฐาน)
curiousguy

6
  1. คุณต้องใช้มันเพื่อสร้าง spinlock เช่นเดียวกับโครงสร้างข้อมูลที่ไม่มีการล็อค (ทั้งหมด?)
  2. ใช้กับการปฏิบัติการ / คำแนะนำของอะตอม
  3. ช่วยฉันหนึ่งครั้งเพื่อเอาชนะข้อผิดพลาดของคอมไพเลอร์ (สร้างรหัสผิดระหว่างการเพิ่มประสิทธิภาพ)

5
คุณจะดีกว่าเมื่อใช้ไลบรารีคอมไพเลอร์ภายในหรือรหัสแอสเซมบลีแบบอินไลน์ ความผันผวนนั้นไม่น่าเชื่อถือ
Zan Lynx

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

ใครพูดอะไรเกี่ยวกับความผันผวนที่ให้ความหมายของอะตอม ฉันบอกว่าคุณจำเป็นต้องใช้ความผันผวนกับการดำเนินงานปรมาณูและถ้าคุณไม่คิดว่ามันเป็นเรื่องจริงที่ประกาศการทำงานประสานของ win32 API (ผู้ชายคนนี้ยังอธิบายเรื่องนี้ในคำตอบของเขา)
Mladen Janković

4

volatileคำหลักที่มีจุดมุ่งหมายเพื่อป้องกันไม่ให้คอมไพเลอร์จากการใช้ optimisations ใด ๆ บนวัตถุที่สามารถเปลี่ยนไปในทางที่ไม่สามารถระบุได้โดยคอมไพเลอร์

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

พิจารณากรณีต่อไปนี้

1) ตัวแปรโกลบอลที่แก้ไขโดยรูทีนเซอร์วิสขัดจังหวะนอกขอบเขต

2) ตัวแปรโกลบอลภายในแอ็พพลิเคชันแบบมัลติเธรด

หากเราไม่ใช้ตัวระบุแบบระเหยได้ปัญหาต่อไปนี้อาจเกิดขึ้น

1) รหัสอาจไม่ทำงานตามที่คาดไว้เมื่อเปิดใช้งานการเพิ่มประสิทธิภาพ

2) รหัสอาจไม่ทำงานอย่างที่คาดไว้เมื่อมีการเปิดใช้งานและใช้งานอินเตอร์รัปต์

ระเหย: เพื่อนที่ดีที่สุดของโปรแกรมเมอร์

https://en.wikipedia.org/wiki/Volatile_(computer_programming)


ลิงค์ที่คุณโพสต์นั้นล้าสมัยมากและไม่ได้สะท้อนแนวปฏิบัติที่ดีที่สุดในปัจจุบัน
Tim Seguine

2

นอกเหนือจากข้อเท็จจริงที่ว่าคำสำคัญระเหยใช้สำหรับบอกคอมไพเลอร์ไม่ปรับการเข้าถึงตัวแปรบางอย่าง (ที่สามารถแก้ไขได้โดยเธรดหรือรูทีนการขัดจังหวะ), มันสามารถใช้เพื่อลบบั๊กคอมไพเลอร์ด้วย - มันสามารถ เป็น ---

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


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

3
จากประสบการณ์ของฉัน 99.9% ของ "บั๊ก" การเพิ่มประสิทธิภาพทั้งหมดใน gcc arm เป็นข้อผิดพลาดในส่วนของโปรแกรมเมอร์ ไม่มีความคิดถ้าสิ่งนี้ใช้ได้กับคำตอบนี้ แค่คุยโวเรื่องทั่วไป
odinthenerd

@Terminus " มันเป็นความผิดพลาดของความคิดที่ว่าคอมไพเลอร์ไม่ได้เพิ่มประสิทธิภาพการเข้าถึงสารระเหย " แหล่งที่มา?
curiousguy

2

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

ดังที่ได้กล่าวไว้ก่อนหน้านี้volatileคำหลักช่วยสำหรับกรณีเช่นนี้

volatile int* p = ...;  // point to some memory
while( *p!=0 ) {}  // loop until the memory becomes zero

แต่ดูเหมือนว่าแทบจะไม่มีผลกระทบเลยเมื่อมีการเรียกใช้ฟังก์ชันภายนอกหรือไม่ใช่แบบอินไลน์ เช่น:

while( *p!=0 ) { g(); }

จากนั้นมีหรือไม่มีvolatileผลลัพธ์เดียวกันเกือบจะถูกสร้างขึ้น

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

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

ดังนั้นฉันแนะนำให้เพิ่มvolatileคำหลักแม้ว่าโปรแกรมของคุณจะทำงานไม่ได้ มันทำให้ความตั้งใจชัดเจนและแข็งแกร่งขึ้นเกี่ยวกับการเปลี่ยนแปลงในอนาคต


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

หลีกเลี่ยง portably inlining การโทร (หรือ "อินไลน์" ของความหมายของมัน) โดยฟังก์ชั่นที่มีร่างกายสามารถมองเห็นได้โดยคอมไพเลอร์ (แม้ในเวลาที่เชื่อมโยงกับการเพิ่มประสิทธิภาพทั่วโลก) เป็นไปได้โดยใช้volatileตัวชี้ฟังก์ชั่นที่มีคุณภาพ:void (* volatile fun_ptr)() = fun; fun_ptr();
curiousguy

2

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

รายละเอียดการเรียงลำดับที่สำคัญจะแตกต่างกันไปตามแพลตฟอร์มเป้าหมายและฟิลด์แอปพลิเคชัน แทนที่จะให้การควบคุมที่มีรายละเอียดเป็นพิเศษมาตรฐานเลือกใช้รูปแบบง่าย ๆ : หากลำดับการเข้าถึงเสร็จสิ้นด้วย lvalues ​​ที่ไม่ผ่านการรับรองvolatileผู้รวบรวมอาจเรียงลำดับใหม่และรวมเข้าด้วยกันตามที่เห็นสมควร หากการดำเนินการเสร็จสิ้นด้วยvolatile-valvalable การใช้งานที่มีคุณภาพควรเสนอการรับประกันการสั่งซื้อเพิ่มเติมใด ๆ ที่อาจจำเป็นต้องใช้โดยรหัสที่กำหนดเป้าหมายแพลตฟอร์มและแอปพลิเคชันที่ต้องการโดยไม่ต้องใช้ไวยากรณ์ที่ไม่ได้มาตรฐาน

น่าเสียดายที่แทนที่จะระบุว่าผู้ค้ำประกันต้องการโปรแกรมเมอร์อะไรคอมไพเลอร์หลายคนเลือกที่จะเสนอการรับประกันขั้นต่ำเปล่าที่ได้รับคำสั่งจากมาตรฐาน สิ่งนี้ทำให้volatileมีประโยชน์น้อยกว่าที่ควรจะเป็น บน gcc หรือเสียงดังกราวด์โปรแกรมเมอร์จำเป็นต้องใช้ mutex พื้นฐาน "hand-off mutex" [ที่งานที่ได้รับและเผยแพร่ mutex จะไม่ทำเช่นนั้นจนกว่างานอื่นจะทำ] ต้องทำ สี่สิ่ง:

  1. ใส่การได้มาและการเปิดตัวของ mutex ในฟังก์ชั่นที่คอมไพเลอร์ไม่สามารถอินไลน์ได้และไม่สามารถใช้การปรับให้เหมาะสมทั้งโปรแกรมได้

  2. ปรับคุณสมบัติของวัตถุทั้งหมดที่มีการปกป้องโดย mutex เป็น - volatileสิ่งที่ไม่ควรจำเป็นหากการเข้าถึงทั้งหมดเกิดขึ้นหลังจากได้รับ mutex และก่อนที่จะปล่อยมัน

  3. ใช้เพิ่มประสิทธิภาพระดับ 0 ถึงบังคับให้คอมไพเลอร์เพื่อสร้างรหัสราวกับว่าวัตถุทั้งหมดที่มีคุณสมบัติไม่เป็นregistervolatile

  4. ใช้คำสั่งเฉพาะ gcc

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

  1. ตรวจสอบให้แน่ใจว่าการvolatileเขียน -qualified ได้รับการดำเนินการทุกที่ที่จำเป็นต้องได้รับหรือปล่อย

การได้รับ "มือปิด mutex" ขั้นพื้นฐานต้องมีการvolatileอ่าน (เพื่อดูว่ามันพร้อม) และไม่ควรต้องมีการvolatileเขียนเช่นกัน (ด้านอื่น ๆ จะไม่พยายามที่จะได้รับมันอีกครั้งจนกว่ามันจะถูกส่งกลับ) แต่ต้อง ดำเนินการvolatileเขียนที่ไม่มีความหมายยังดีกว่าตัวเลือกใด ๆ ที่มีอยู่ภายใต้ gcc หรือเสียงดังกราว


1

การใช้งานครั้งเดียวที่ฉันควรเตือนคุณคือในฟังก์ชั่นตัวจัดการสัญญาณหากคุณต้องการเข้าถึง / แก้ไขตัวแปรกลาง (เช่นทำเครื่องหมายเป็น exit = true) คุณต้องประกาศตัวแปรนั้นว่า 'ระเหย'


1

คำตอบทั้งหมดนั้นยอดเยี่ยม แต่ยิ่งไปกว่านั้นฉันต้องการแบ่งปันตัวอย่าง

ด้านล่างเป็นโปรแกรม cpp เล็กน้อย:

#include <iostream>

int x;

int main(){
    char buf[50];
    x = 8;

    if(x == 8)
        printf("x is 8\n");
    else
        sprintf(buf, "x is not 8\n");

    x=1000;
    while(x > 5)
        x--;
    return 0;
}

ตอนนี้ให้สร้างชุดประกอบของโค้ดด้านบน (และฉันจะวางเฉพาะส่วนของชุดประกอบที่เกี่ยวข้องที่นี่):

คำสั่งเพื่อสร้างชุดประกอบ:

g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp

และการชุมนุม:

main:
.LFB1594:
    subq    $40, %rsp    #,
    .seh_stackalloc 40
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC0(%rip), %rcx     #,
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:10:         printf("x is 8\n");
    call    _ZL6printfPKcz.constprop.0   #
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    movl    $5, x(%rip)  #, x
    addq    $40, %rsp    #,
    ret 
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

คุณสามารถเห็นได้ในแอสเซมบลีที่ไม่ได้สร้างรหัสแอสเซมบลีsprintfเนื่องจากคอมไพเลอร์สันนิษฐานว่าxจะไม่เปลี่ยนแปลงภายนอกของโปรแกรม และเป็นกรณีเดียวกันกับwhileวง whileห่วงถูกลบออกไปโดยสิ้นเชิงเนื่องจากการเพิ่มประสิทธิภาพของคอมไพเลอร์เพราะเห็นว่ามันเป็นรหัสที่ไร้ประโยชน์และทำให้ได้รับมอบหมายโดยตรง5ไปx(ดูmovl $5, x(%rip))

ปัญหาเกิดขึ้นเมื่อสิ่งที่ถ้ากระบวนการภายนอก / ฮาร์ดแวร์จะเปลี่ยนค่าของxบางระหว่างและx = 8; if(x == 8)เราคาดว่าการelseบล็อกจะทำงาน แต่น่าเสียดายที่ผู้รวบรวมได้ตัดส่วนนั้นออก

ตอนนี้เพื่อที่จะแก้ปัญหานี้ในassembly.cppให้เราเปลี่ยนint x;ไปvolatile int x;ได้อย่างรวดเร็วและเห็นรหัสการชุมนุมที่สร้าง:

main:
.LFB1594:
    subq    $104, %rsp   #,
    .seh_stackalloc 104
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:9:     if(x == 8)
    movl    x(%rip), %eax    # x, x.1_1
 # assembly.cpp:9:     if(x == 8)
    cmpl    $8, %eax     #, x.1_1
    je  .L11     #,
 # assembly.cpp:12:         sprintf(buf, "x is not 8\n");
    leaq    32(%rsp), %rcx   #, tmp93
    leaq    .LC0(%rip), %rdx     #,
    call    _ZL7sprintfPcPKcz.constprop.0    #
.L7:
 # assembly.cpp:14:     x=1000;
    movl    $1000, x(%rip)   #, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_15
    cmpl    $5, %eax     #, x.3_15
    jle .L8  #,
    .p2align 4,,10
.L9:
 # assembly.cpp:16:         x--;
    movl    x(%rip), %eax    # x, x.4_3
    subl    $1, %eax     #, _4
    movl    %eax, x(%rip)    # _4, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_2
    cmpl    $5, %eax     #, x.3_2
    jg  .L9  #,
.L8:
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    addq    $104, %rsp   #,
    ret 
.L11:
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC1(%rip), %rcx     #,
    call    _ZL6printfPKcz.constprop.1   #
    jmp .L7  #
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

ที่นี่คุณสามารถเห็นได้ว่ารหัสประกอบsprintf, printfและwhileห่วงถูกสร้างขึ้น ข้อได้เปรียบคือถ้าxตัวแปรถูกเปลี่ยนแปลงโดยบางโปรแกรมหรือฮาร์ดแวร์ภายนอกบางsprintfส่วนของรหัสจะถูกดำเนินการ และwhileวนซ้ำในทำนองเดียวกันสามารถใช้สำหรับการรอไม่ว่างในขณะนี้


0

คำตอบอื่น ๆ แล้วพูดถึงการหลีกเลี่ยงการเพิ่มประสิทธิภาพเพื่อ:

  • ใช้การลงทะเบียนหน่วยความจำที่แมป (หรือ "MMIO")
  • เขียนไดรเวอร์อุปกรณ์
  • ช่วยให้การดีบักโปรแกรมง่ายขึ้น
  • ทำให้การคำนวณจุดลอยตัวกำหนดขึ้นได้มากขึ้น

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

อ่านความผันผวนเป็นเหมือนการดำเนินการป้อนข้อมูล (ชอบscanfหรือใช้cin): ค่าที่ดูเหมือนว่าจะมาจากด้านนอกของโปรแกรมเพื่อการคำนวณใด ๆ ที่มีการพึ่งพาความต้องการค่าที่จะเริ่มต้นหลังจากที่มัน

เขียนความผันผวนเป็นเหมือนการดำเนินการส่งออก (ชอบprintfหรือใช้cout): มูลค่าที่ดูเหมือนว่าจะมีการสื่อสารด้านนอกของโปรแกรมดังนั้นหากค่าขึ้นอยู่กับการคำนวณก็จะต้องมีการดำเนินการเสร็จสิ้นก่อน

ดังนั้นคู่ของความผันผวนอ่าน / เขียนสามารถใช้ในการเชื่องมาตรฐานและทำให้การวัดเวลาที่มีความหมาย

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

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