num ++ สามารถเป็นอะตอมสำหรับ 'int num' ได้หรือไม่


153

โดยทั่วไปสำหรับint num, num++(หรือ++num), ในฐานะการดำเนินการอ่าน - แก้ไข - การเขียนไม่ได้เป็นอะตอมมิก แต่ฉันมักจะเห็นคอมไพเลอร์เช่นGCCสร้างรหัสต่อไปนี้ ( ลองที่นี่ ):

ป้อนคำอธิบายภาพที่นี่

ตั้งแต่บรรทัดที่ 5 ซึ่งสอดคล้องกับnum++คำสั่งเดียวเราสามารถสรุปได้ว่าnum++ เป็นอะตอมในกรณีนี้หรือไม่?

และถ้าเป็นเช่นนั้นหมายความว่าสิ่งที่สร้างขึ้นnum++สามารถนำมาใช้ในสถานการณ์พร้อมกัน (มัลติเธรด) โดยไม่มีอันตรายจากการแข่งขันของข้อมูล (เช่นเราไม่จำเป็นต้องทำตัวอย่างเช่นstd::atomic<int>และกำหนดค่าใช้จ่ายที่เกี่ยวข้องเนื่องจากเป็น ปรมาณูอยู่แล้ว)?

UPDATE

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


65
ใครบอกคุณว่าaddเป็นอะตอม
Slava

6
เนื่องจากหนึ่งในคุณสมบัติของอะตอมมิกส์คือการป้องกันการเรียงลำดับใหม่ในระหว่างการปรับให้เหมาะสมโดยไม่คำนึงถึงอะตอมมิกของการทำงานจริง
jaggedSpire

19
ฉันอยากจะชี้ให้เห็นว่าถ้านี่เป็นอะตอมมิกบนแพลตฟอร์มของคุณไม่มีการรับประกันว่ามันจะเป็นในรูปแบบอื่น std::atomic<int>เป็นอิสระแพลตฟอร์มและแสดงความตั้งใจของคุณโดยใช้
NathanOliver

8
ในระหว่างการดำเนินการตามaddคำสั่งนั้นแกนอีกแกนหนึ่งอาจขโมยที่อยู่หน่วยความจำนั้นจากแคชของแกนนี้และแก้ไขได้ บน CPU x86 addคำสั่งนั้นจำเป็นต้องมีlockคำนำหน้าถ้าที่อยู่จะต้องถูกล็อคในแคชในช่วงระยะเวลาของการดำเนินการ
David Schwartz

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

คำตอบ:


197

นี่คือสิ่งที่ C ++ กำหนดว่าเป็น Data Race ที่ทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดแม้ว่าคอมไพเลอร์ตัวหนึ่งจะเกิดขึ้นเพื่อสร้างโค้ดที่ทำในสิ่งที่คุณหวังไว้ในเครื่องเป้าหมาย คุณต้องใช้std::atomicเพื่อให้ได้ผลลัพธ์ที่เชื่อถือได้ แต่คุณสามารถใช้กับมันได้memory_order_relaxedหากคุณไม่สนใจการเรียงลำดับใหม่ ดูด้านล่างสำหรับตัวอย่างรหัสบางอย่างและเอาท์พุท asm fetch_addใช้


แต่ก่อนอื่นภาษาแอสเซมบลีเป็นส่วนหนึ่งของคำถาม:

เนื่องจาก num ++ เป็นคำสั่งเดียว ( add dword [num], 1) เราสามารถสรุปได้ว่า num ++ เป็นอะตอมมิกในกรณีนี้หรือไม่

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

การดำเนินการของหน่วยความจำจาก CPU อื่นสามารถมองเห็นได้ทั่วโลกระหว่างโหลดและจัดเก็บ นั่นคือสองเธรดที่ทำงานadd dword [num], 1ในลูปจะเข้าสู่ร้านค้าของกันและกัน (ดูคำตอบของ @ Margaretสำหรับแผนผังที่ดี) หลังจากเพิ่ม 40k จากแต่ละเธรดสองเธรดตัวนับอาจเพิ่มขึ้น ~ 60k (ไม่ใช่ 80k) บนฮาร์ดแวร์ x86 แบบมัลติคอร์จริงเท่านั้น


"Atomic" จากคำภาษากรีกแปลว่าแบ่งแยกไม่ได้หมายความว่าผู้สังเกตการณ์ไม่สามารถมองเห็นการดำเนินการเป็นขั้นตอนแยกกันได้ การเกิดขึ้นทั้งทางร่างกาย / ทางไฟฟ้าทันทีสำหรับบิตทั้งหมดพร้อมกันเป็นเพียงวิธีหนึ่งในการทำสิ่งนี้ให้สำเร็จสำหรับการโหลดหรือการจัดเก็บ แต่นั่นก็ไม่สามารถทำได้สำหรับการดำเนินการ ALU ฉันได้เข้าไปดูรายละเอียดเพิ่มเติมเกี่ยวกับการโหลดที่บริสุทธิ์และร้านค้าที่บริสุทธิ์ในคำตอบของฉันเกี่ยวกับ Atomicity ใน x86ในขณะที่คำตอบนี้เน้นที่การอ่าน - แก้ไข - เขียน

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

ดังนั้นlock add dword [num], 1 เป็นอะตอม แกน CPU ที่รันคำสั่งนั้นจะเก็บสายแคชไว้ในสถานะ Modified ในแคช L1 ส่วนตัวจากเมื่อโหลดอ่านข้อมูลจากแคชจนกว่าที่เก็บจะยอมรับผลลัพธ์ของมันกลับสู่แคช สิ่งนี้จะช่วยป้องกันไม่ให้แคชอื่น ๆ ในระบบมีสำเนาของแคชบรรทัด ณ จุดใด ๆ จากการโหลดไปยังที่จัดเก็บตามกฎของโปรโตคอลการเชื่อมโยงแคช MESI (หรือรุ่น MOESI / MESIF ที่ใช้โดย multi-core AMD / Intel CPUs ตามลำดับ) ดังนั้นการดำเนินการโดยแกนอื่น ๆ ดูเหมือนจะเกิดขึ้นก่อนหรือหลังไม่ใช่ระหว่าง

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

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

โปรดทราบว่าlockคำนำหน้ายังเปลี่ยนคำสั่งเป็นกำแพงหน่วยความจำเต็ม (เช่นMFENCE ) หยุดการจัดเรียงเวลาทำงานใหม่ทั้งหมดและให้ความสอดคล้องตามลำดับ (ดูโพสต์บล็อกเจฟฟ์ Preshing ยอดเยี่ยม . โพสต์อื่น ๆ ของเขาเป็นอย่างดีในทุกเกินไปอย่างชัดเจนและอธิบายมากของสิ่งที่ดีเกี่ยวกับการเขียนโปรแกรมล็อคฟรีจาก x86 และรายละเอียดฮาร์ดแวร์อื่น ๆ กับกฎระเบียบของ C ++.)


บนเครื่องยูนิโพรเซสเซอร์หรือในกระบวนการแบบเธรดเดี่ยวคำสั่งRMWเดียวจริงๆแล้วคืออะตอมมิกโดยไม่มีlockคำนำหน้า วิธีเดียวสำหรับรหัสอื่นในการเข้าถึงตัวแปรที่ใช้ร่วมกันคือเพื่อให้ CPU ทำการสลับบริบทซึ่งไม่สามารถเกิดขึ้นได้ในระหว่างการเรียนการสอน ดังนั้นธรรมดาdec dword [num]สามารถซิงโครไนซ์ระหว่างโปรแกรมแบบเธรดเดี่ยวและตัวจัดการสัญญาณหรือในโปรแกรมแบบมัลติเธรดที่ทำงานบนเครื่องแกนเดียว ดูครึ่งหลังของคำตอบของฉันสำหรับคำถามอื่นและความคิดเห็นที่อยู่ใต้นั้นซึ่งฉันอธิบายในรายละเอียดเพิ่มเติม


กลับไปที่ C ++:

เป็นการใช้ทั้งหมดnum++โดยไม่แจ้งให้คอมไพเลอร์ทราบว่าคุณต้องการรวบรวมเพื่อนำไปใช้งานอ่าน - แก้ไข - เขียน:

;; Valid compiler output for num++
mov   eax, [num]
inc   eax
mov   [num], eax

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

(หากไม่ต้องการค่าในภายหลังขอinc dword [num]แนะนำซีพียู x86 ที่ทันสมัยจะเรียกใช้คำสั่ง RMW หน่วยความจำปลายทางอย่างน้อยมีประสิทธิภาพเท่ากับการใช้สามคำสั่งแยกกันสนุกจริง ๆ : gcc -O3 -m32 -mtune=i586จะปล่อยสิ่งนี้จริง ๆเพราะเพนเทอร์เซียมของ P5 ไม่ต้องถอดรหัสคำแนะนำที่ซับซ้อนให้กับการทำงานจุลภาคแบบง่าย ๆ หลาย ๆ วิธี P6 และสถาปัตยกรรมจุลภาคในภายหลังดูคำแนะนำของตารางคำแนะนำ / สถาปัตยกรรมจุลภาคของ Agner Fogสำหรับข้อมูลเพิ่มเติมและ ติดแท็ก wiki สำหรับลิงก์ที่มีประโยชน์มากมาย (รวมถึงคู่มือ ISA ของ Intel x86 ซึ่งมีให้บริการฟรีในรูปแบบ PDF)


อย่าสับสนรุ่นหน่วยความจำเป้าหมาย (x86) กับรุ่นหน่วยความจำ C ++

การจัดเรียงใหม่รวบรวมเวลาที่ได้รับอนุญาต อีกส่วนหนึ่งของสิ่งที่คุณได้รับจาก std :: atomic คือการควบคุมการเรียงลำดับเวลาใหม่เพื่อให้แน่ใจว่าคุณnum++สามารถมองเห็นได้ทั่วโลกหลังจากการดำเนินการอื่น ๆ

ตัวอย่างแบบคลาสสิก: การจัดเก็บข้อมูลบางอย่างลงในบัฟเฟอร์เพื่อให้เธรดอื่นดูแล้วตั้งค่าสถานะ แม้ว่า x86 ไม่โหลดซื้อ / flag.store(1, std::memory_order_release);ร้านค้าปล่อยฟรีคุณยังคงต้องบอกคอมไพเลอร์ไม่ได้ที่จะสั่งซื้อใหม่โดยใช้

คุณอาจคาดหวังว่ารหัสนี้จะซิงโครไนซ์กับเธรดอื่น ๆ :

// flag is just a plain int global, not std::atomic<int>.
flag--;       // This isn't a real lock, but pretend it's somehow meaningful.
modify_a_data_structure(&foo);    // doesn't look at flag, and the compilers knows this.  (Assume it can see the function def).  Otherwise the usual don't-break-single-threaded-code rules come into play!
flag++;

แต่มันจะไม่ คอมไพเลอร์มีอิสระที่จะย้ายflag++ข้ามการเรียกใช้ฟังก์ชัน (ถ้ามันอินไลน์ฟังก์ชั่นหรือรู้ว่ามันไม่ได้ดูflag) จากนั้นก็จะสามารถเพิ่มประสิทธิภาพการปรับเปลี่ยนออกไปอย่างสิ้นเชิงเพราะไม่ได้flag volatile(และไม่มี c ++ volatileไม่ได้เป็นตัวแทนที่มีประโยชน์สำหรับมาตรฐาน :: อะตอม. มาตรฐาน :: อะตอมจะทำให้คอมไพเลอร์ถือว่าค่าที่ในหน่วยความจำสามารถแก้ไขได้ถ่ายทอดสดคล้ายกับvolatileแต่มีมากขึ้นไปกว่านั้น. ยังvolatile std::atomic<int> fooไม่ได้เป็น เช่นเดียวกับstd::atomic<int> fooที่หารือกับ @Richard Hodges)

การกำหนดข้อมูลการแข่งขันในตัวแปรที่ไม่ใช่อะตอมมิกเป็น Undefined Behavior คือสิ่งที่ช่วยให้คอมไพเลอร์ยังคงยกโหลดและเก็บออกจากลูปและการเพิ่มประสิทธิภาพอื่น ๆ อีกมากมายสำหรับหน่วยความจำที่หลายเธรดอาจมีการอ้างอิง (ดูบล็อก LLVM นี้สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีที่ UB เปิดใช้งานการปรับให้เหมาะสมของคอมไพเลอร์)


ดังที่ฉันกล่าวถึงคำนำหน้าx86lockเป็นอุปสรรคหน่วยความจำเต็มดังนั้นการใช้num.fetch_add(1, std::memory_order_relaxed);สร้างรหัสเดียวกันบน x86 เป็นnum++(ค่าเริ่มต้นคือความสอดคล้องตามลำดับ) แต่มันมีประสิทธิภาพมากขึ้นในสถาปัตยกรรมอื่น ๆ (เช่น ARM) แม้แต่ใน x86 การผ่อนคลายยังอนุญาตให้ทำการเรียงลำดับเวลาใหม่ได้มากขึ้น

นี่คือสิ่งที่ GCC ทำกับ x86 สำหรับฟังก์ชั่นบางอย่างที่ทำงานกับstd::atomicตัวแปรทั่วโลก

ดูแหล่งที่มาชุมนุม + รหัสภาษาที่จัดรูปแบบเป็นอย่างดีในคอมไพเลอร์สำรวจ Godbolt คุณสามารถเลือกสถาปัตยกรรมเป้าหมายอื่น ๆ รวมถึง ARM, MIPS และ PowerPC เพื่อดูรหัสภาษาแอสเซมบลีชนิดใดที่คุณได้รับจากอะตอมมิกส์สำหรับเป้าหมายเหล่านั้น

#include <atomic>
std::atomic<int> num;
void inc_relaxed() {
  num.fetch_add(1, std::memory_order_relaxed);
}

int load_num() { return num; }            // Even seq_cst loads are free on x86
void store_num(int val){ num = val; }
void store_num_release(int val){
  num.store(val, std::memory_order_release);
}
// Can the compiler collapse multiple atomic operations into one? No, it can't.

# g++ 6.2 -O3, targeting x86-64 System V calling convention. (First argument in edi/rdi)
inc_relaxed():
    lock add        DWORD PTR num[rip], 1      #### Even relaxed RMWs need a lock. There's no way to request just a single-instruction RMW with no lock, for synchronizing between a program and signal handler for example. :/ There is atomic_signal_fence for ordering, but nothing for RMW.
    ret
inc_seq_cst():
    lock add        DWORD PTR num[rip], 1
    ret
load_num():
    mov     eax, DWORD PTR num[rip]
    ret
store_num(int):
    mov     DWORD PTR num[rip], edi
    mfence                          ##### seq_cst stores need an mfence
    ret
store_num_release(int):
    mov     DWORD PTR num[rip], edi
    ret                             ##### Release and weaker doesn't.
store_num_relaxed(int):
    mov     DWORD PTR num[rip], edi
    ret

ขอให้สังเกตว่าจำเป็นต้องใช้ MFENCE (กำแพงกั้นเต็มรูปแบบ) หลังจากร้านค้าที่มีความสอดคล้องตามลำดับ โดยทั่วไปแล้วจะมีการสั่ง x86 อย่างยิ่ง แต่อนุญาตให้ทำการเรียงลำดับ StoreLoad ใหม่ได้ การมีบัฟเฟอร์ของร้านค้านั้นเป็นสิ่งจำเป็นสำหรับประสิทธิภาพที่ดีของซีพียูที่ไม่ทำงานตามสั่ง หน่วยความจำการจัดเรียงใหม่ของJeff Preshing ใน Actแสดงผลที่ตามมาจากการไม่ใช้ MFENCE พร้อมรหัสจริงเพื่อแสดงการจัดเรียงใหม่ที่เกิดขึ้นบนฮาร์ดแวร์จริง


Re: การอภิปรายในความคิดเห็นเกี่ยวกับคำตอบ @Richard Hodges เกี่ยวกับคอมไพเลอร์ที่รวม std :: atomic num++; num-=2;operation ไว้ในnum--;คำสั่งเดียว :

คำถามและคำตอบแยกต่างหากในหัวข้อเดียวกันนี้: ทำไมจึงไม่คอมไพเลอร์รวม std ซ้ำซ้อน :: atomic write? ที่คำตอบของฉันคืนสิ่งที่ฉันเขียนด้านล่างมากมาย

คอมไพเลอร์ปัจจุบันยังไม่ได้ทำสิ่งนี้ (แต่) แต่ไม่ใช่เพราะพวกเขาไม่ได้รับอนุญาต C ++ WG21 / P0062R1: เมื่อใดที่คอมไพเลอร์ควรปรับแต่งอะตอมมิกให้เหมาะสม? กล่าวถึงความคาดหวังว่าโปรแกรมเมอร์หลายคนมีคอมไพเลอร์จะไม่ทำการเพิ่มประสิทธิภาพ "น่าประหลาดใจ" และสิ่งที่มาตรฐานสามารถทำได้เพื่อให้โปรแกรมเมอร์ควบคุม N4455กล่าวถึงตัวอย่างมากมายของสิ่งต่าง ๆ ที่สามารถปรับให้เหมาะสมรวมถึงอันนี้ มันชี้ให้เห็นว่าการทำอินไลน์และการแพร่กระจายอย่างต่อเนื่องสามารถแนะนำสิ่งต่าง ๆ เช่นfetch_or(0)ที่อาจจะกลายเป็นเพียงแค่load()(แต่ยังคงได้รับและเผยแพร่ความหมาย) แม้ว่าแหล่งต้นฉบับไม่ได้มี ops อะตอมที่ซ้ำซ้อนอย่างเห็นได้ชัด

เหตุผลที่แท้จริงที่คอมไพเลอร์ไม่ได้ทำ (ยัง) คือ: (1) ไม่มีใครเขียนโค้ดที่ซับซ้อนที่จะอนุญาตให้คอมไพเลอร์ทำอย่างปลอดภัย (โดยที่ไม่ผิด) และ (2) อาจละเมิดหลักการอย่างน้อย แปลกใจ รหัสที่ล็อคได้ยากพอที่จะเขียนอย่างถูกต้องตั้งแต่แรก ดังนั้นอย่าคิดมากกับการใช้อาวุธปรมาณู: พวกมันไม่ถูกและไม่เพิ่มประสิทธิภาพมากนัก ไม่ใช่เรื่องง่ายเสมอไปที่จะหลีกเลี่ยงการทำงานของอะตอมที่ซ้ำซ้อนด้วยstd::shared_ptr<T>เนื่องจากไม่มีรุ่นที่ไม่ใช่อะตอม (แม้ว่าคำตอบอย่างใดอย่างหนึ่งที่นี่ให้วิธีที่ง่ายในการกำหนด a shared_ptr_unsynchronized<T>gcc)


เดินทางกลับไปnum++; num-=2;รวบรวมราวกับว่ามันถูกnum--: คอมไพเลอร์จะได้รับอนุญาตที่จะทำนี้เว้นแต่เป็นnum volatile std::atomic<int>หากสามารถจัดลำดับใหม่ได้กฎ as-if อนุญาตให้คอมไพเลอร์ตัดสินใจ ณ เวลารวบรวมที่มันเกิดขึ้นเสมอ ไม่มีสิ่งใดรับประกันได้ว่าผู้สังเกตการณ์จะเห็นค่ากลาง ( num++ผลลัพธ์)

เช่นถ้าสั่งซื้อสินค้าที่ไม่มีอะไรจะปรากฏทั่วโลกระหว่างการดำเนินการเหล่านี้เข้ากันได้กับความต้องการสั่งซื้อของแหล่งที่มา (ตามไปที่ C ++ กฎสำหรับเครื่องนามธรรมไม่สถาปัตยกรรมเป้าหมาย) คอมไพเลอร์สามารถปล่อยซิงเกิ้ลlock dec dword [num]แทน/lock inc dword [num]lock sub dword [num], 2

num++; num--ไม่สามารถหายไปได้เพราะมันยังคงมีซิงโครไนซ์กับความสัมพันธ์กับเธรดอื่น ๆ ที่มองnumและเป็นทั้งโหลดโหลดและรีลีสสโตร์ซึ่งไม่สามารถเรียงลำดับการดำเนินการอื่นในเธรดนี้ได้ สำหรับ x86 สิ่งนี้อาจรวบรวมเป็น MFENCE แทนที่จะเป็นlock add dword [num], 0(เช่นnum += 0)

ดังที่กล่าวไว้ในPR0062การรวมกันของ ops ปรมาณูที่ไม่ได้อยู่ใกล้เคียงกันมากขึ้นในเวลารวบรวมอาจไม่ดี (เช่นตัวนับความคืบหน้าจะได้รับการอัปเดตเพียงครั้งเดียวในตอนท้ายแทนที่จะเป็นซ้ำทุกครั้ง) แต่ก็สามารถช่วย atomic inc / dec of ref จะนับเมื่อสำเนาของ a shared_ptrถูกสร้างและทำลายถ้าคอมไพเลอร์สามารถพิสูจน์ได้ว่าshared_ptrมีวัตถุอื่นอยู่สำหรับอายุการใช้งานทั้งหมดของชั่วคราว)

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


ด้วยปัจจุบัน gcc6.2 และ clang3.9 คุณยังคงได้รับlockการดำเนินการแยกต่างหากแม้memory_order_relaxedในกรณีที่ปรับให้เหมาะสมที่สุดอย่างเห็นได้ชัด ( Godbolt compiler explorerเพื่อให้คุณสามารถดูว่ารุ่นล่าสุดแตกต่างกันหรือไม่)

void multiple_ops_relaxed(std::atomic<unsigned int>& num) {
  num.fetch_add( 1, std::memory_order_relaxed);
  num.fetch_add(-1, std::memory_order_relaxed);
  num.fetch_add( 6, std::memory_order_relaxed);
  num.fetch_add(-5, std::memory_order_relaxed);
  //num.fetch_add(-1, std::memory_order_relaxed);
}

multiple_ops_relaxed(std::atomic<unsigned int>&):
    lock add        DWORD PTR [rdi], 1
    lock sub        DWORD PTR [rdi], 1
    lock add        DWORD PTR [rdi], 6
    lock sub        DWORD PTR [rdi], 5
    ret

1
"[ใช้คำสั่งแยกต่างหาก] เคยมีประสิทธิภาพมากกว่า ... แต่ซีพียู x86 ที่ทันสมัยจัดการกับการดำเนินงาน RMW อย่างน้อยก็อย่างมีประสิทธิภาพ" อีกครั้ง - มันยังคงมีประสิทธิภาพมากกว่าในกรณีที่ค่าที่อัปเดตจะถูกนำมาใช้ภายหลังในฟังก์ชันเดียวกัน และมีการลงทะเบียนฟรีสำหรับคอมไพเลอร์เพื่อเก็บไว้ใน (และตัวแปรไม่ได้ทำเครื่องหมายความผันผวนแน่นอน) ซึ่งหมายความว่ามีความเป็นไปได้สูงที่ว่าคอมไพเลอร์สร้างคำสั่งเดียวหรือหลายอย่างสำหรับการดำเนินการขึ้นอยู่กับส่วนที่เหลือของรหัสในฟังก์ชั่นไม่ใช่แค่บรรทัดเดียวในคำถาม
Periata Breatta

@PerataBreatta: ใช่จุดดี ใน asm คุณสามารถใช้mov eax, 1 xadd [num], eax(โดยไม่มีคำนำหน้าการล็อก) เพื่อใช้การเพิ่มภายหลังnum++แต่นั่นไม่ใช่สิ่งที่คอมไพเลอร์ทำ
Peter Cordes

3
@ DavidC.Rankin: หากคุณมีการแก้ไขใด ๆ ที่คุณต้องการทำรู้สึกฟรี ฉันไม่ต้องการที่จะทำให้ CW นี้ มันยังคงเป็นงานของฉัน (และระเบียบ: P) ฉันจะเป็นระเบียบเรียบร้อยขึ้นหลังจาก [ร่อน] เกมที่ดีที่สุดของฉัน :)
ปีเตอร์ Cordes

1
หากไม่ใช่วิกิชุมชนอาจลิงก์ไปยังแท็กวิกิที่เหมาะสม (ทั้ง x86 และแท็กอะตอมมิก?) มันคุ้มค่ากับการเชื่อมโยงเพิ่มเติมแทนที่จะกลับมาอย่างมีความหวังโดยการค้นหาทั่วไปใน SO (ถ้าฉันรู้ดีกว่าว่าควรจะทำอย่างไรในเรื่องนั้นฉันจะทำมันฉันจะต้องขุดลึกลงไปในสิ่งที่ไม่ควรทำ วิกิลิงก์)
David C. Rankin

1
เช่นเคย - คำตอบที่ดี! ความแตกต่างที่ดีระหว่างการเชื่อมโยงกันและ atomicity (ที่บางคนอื่น ๆ ได้มันผิด)
Leeor

39

... และตอนนี้เรามาเปิดใช้งานการเพิ่มประสิทธิภาพ:

f():
        rep ret

ตกลงให้โอกาส:

void f(int& num)
{
  num = 0;
  num++;
  --num;
  num += 6;
  num -=5;
  --num;
}

ผลลัพธ์:

f(int&):
        mov     DWORD PTR [rdi], 0
        ret

การสังเกตเธรดอื่น (แม้จะเพิกเฉยต่อความล่าช้าในการซิงโครไนซ์แคช) ก็ไม่มีโอกาสสังเกตการเปลี่ยนแปลงรายบุคคล

เปรียบเทียบกับ:

#include <atomic>

void f(std::atomic<int>& num)
{
  num = 0;
  num++;
  --num;
  num += 6;
  num -=5;
  --num;
}

เมื่อผลลัพธ์คือ:

f(std::atomic<int>&):
        mov     DWORD PTR [rdi], 0
        mfence
        lock add        DWORD PTR [rdi], 1
        lock sub        DWORD PTR [rdi], 1
        lock add        DWORD PTR [rdi], 6
        lock sub        DWORD PTR [rdi], 5
        lock sub        DWORD PTR [rdi], 1
        ret

ตอนนี้การปรับเปลี่ยนแต่ละครั้งคือ: -

  1. สามารถสังเกตได้ในเธรดอื่นและ
  2. เคารพการแก้ไขที่คล้ายกันที่เกิดขึ้นในหัวข้ออื่น ๆ

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

ข้อมูลเพิ่มเติม

เกี่ยวกับผลของการปรับการปรับปรุงของstd::atomics

มาตรฐาน c ++ มีกฎ 'ราวกับว่า' ซึ่งอนุญาตให้คอมไพเลอร์เรียงลำดับรหัสใหม่และแม้แต่เขียนรหัสใหม่อีกครั้งโดยที่ผลลัพธ์นั้นมีผลที่สังเกตได้เหมือนกัน (รวมถึงผลข้างเคียง) ราวกับว่าคุณทำ รหัส.

กฎราวกับว่าเป็นอนุรักษ์นิยมโดยเฉพาะอย่างยิ่งที่เกี่ยวข้องกับอะตอม

พิจารณา:

void incdec(int& num) {
    ++num;
    --num;
}

เนื่องจากไม่มีการล็อค mutex, atomics หรือโครงสร้างอื่น ๆ ที่มีผลต่อการเรียงลำดับระหว่างเธรดฉันจะยืนยันว่าคอมไพเลอร์มีอิสระที่จะเขียนฟังก์ชันนี้ใหม่เป็น NOP เช่น:

void incdec(int&) {
    // nada
}

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

อย่างไรก็ตามนี่เป็นเกมบอลที่แตกต่าง:

void incdec(std::atomic<int>& num) {
    ++num;
    --num;
}

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

นี่คือตัวอย่าง:

#include <thread>
#include <atomic>

int main()
{
    for (int iter = 0 ; iter < 20 ; ++iter)
    {
        std::atomic<int> num = { 0 };
        std::thread t1([&] {
            for (int i = 0 ; i < 10000000 ; ++i)
            {
                ++num;
                --num;
            }
        });
        std::thread t2([&] {
            for (int i = 0 ; i < 10000000 ; ++i)
            {
                num = 100;
            }
        });
        
        t2.join();
        t1.join();
        std::cout << num << std::endl;
    }
}

ตัวอย่างผลลัพธ์:

99
99
99
99
99
100
99
99
100
100
100
100
99
99
100
99
99
100
100
99

5
นี้ล้มเหลวที่จะอธิบายว่าadd dword [rdi], 1เป็นไม่ได้อะตอม (โดยไม่มีlockคำนำหน้า) โหลดคืออะตอมมิกและที่เก็บเป็นอะตอมมิก แต่ไม่มีอะไรหยุดเธรดอื่นจากการแก้ไขข้อมูลระหว่างโหลดและที่จัดเก็บ ดังนั้นทางร้านจึงสามารถทำการดัดแปลงโดยเธรดอื่นได้ ดูjfdube.wordpress.com/2011/11/30/understanding-atomic-operations นอกจากนี้ บทความปลอดล็อคของ Jeff Preshing นั้นดีมากและเขาพูดถึงปัญหา RMW พื้นฐานในบทความบทนำนั้น
Peter Cordes

3
สิ่งที่เกิดขึ้นจริงที่นี่คือไม่มีใครใช้การเพิ่มประสิทธิภาพนี้ใน gcc เพราะมันเกือบจะไร้ประโยชน์และอาจเป็นอันตรายมากกว่าที่เป็นประโยชน์ (หลักการของความประหลาดใจอย่างน้อย. อาจจะมีคนถูกคาดหวังว่ารัฐชั่วคราวเพื่อสามารถมองเห็นได้ในบางครั้งและจะตกลงกับ probabilty สถิติ. หรือพวกเขาจะใช้ฮาร์ดแวร์ดูจุดที่จะขัดขวางในการปรับเปลี่ยน.) ล๊ความต้องการของรหัสที่จะถูกสร้างขึ้นมาอย่างระมัดระวัง ดังนั้นจะไม่มีสิ่งใดที่จะปรับให้เหมาะสม มันอาจจะมีประโยชน์ในการค้นหาและพิมพ์คำเตือนเพื่อเตือน coder ว่ารหัสของพวกเขาอาจไม่ได้หมายถึงสิ่งที่พวกเขาคิด!
Peter Cordes

2
นั่นอาจเป็นเหตุผลสำหรับคอมไพเลอร์ที่จะไม่ใช้สิ่งนี้ (หลักการของความประหลาดใจน้อยที่สุดและอื่น ๆ ) การสังเกตว่าจะเป็นไปได้ในการปฏิบัติเกี่ยวกับฮาร์ดแวร์จริง อย่างไรก็ตามกฎการสั่งซื้อหน่วยความจำ C ++ ไม่ได้พูดอะไรเกี่ยวกับการรับประกันใด ๆ ว่าโหลดหนึ่งเธรดจะผสม "เท่ากัน" กับ ops ของเธรดอื่นในเครื่องนามธรรม C ++ ฉันยังคิดว่ามันจะถูกกฎหมาย แต่โปรแกรมเมอร์เป็นศัตรู
Peter Cordes

2
การทดลองทางความคิด: พิจารณาการใช้ C ++ ในระบบมัลติทาสกิ้งแบบร่วมมือกัน มันใช้ std :: thread โดยการใส่คะแนนผลผลิตที่จำเป็นเพื่อหลีกเลี่ยงการหยุดชะงัก แต่ไม่ใช่ระหว่างทุกคำสั่ง ผมคิดว่าคุณจะเถียงว่าบางสิ่งบางอย่างใน C ++ มาตรฐานต้องมีจุดที่อัตราผลตอบแทนระหว่างและ num++ หากคุณสามารถหาหัวข้อในมาตรฐานที่กำหนดได้ ฉันค่อนข้างมั่นใจว่าต้องการเพียงผู้สังเกตการณ์เท่านั้นที่ไม่สามารถเห็นการจัดลำดับใหม่อย่างผิดพลาดซึ่งไม่ต้องการผลตอบแทนที่นั่น ดังนั้นฉันคิดว่ามันเป็นเพียงปัญหาคุณภาพเท่านั้น num--
Peter Cordes

5
เพื่อประโยชน์ของการสิ้นสุดฉันถามในรายชื่อผู้รับจดหมายการอภิปราย std คำถามนี้เกิดขึ้น 2 บทความซึ่งดูเหมือนว่าทั้งสองเห็นพ้องกับ Peter และพูดถึงข้อกังวลที่ฉันมีเกี่ยวกับการเพิ่มประสิทธิภาพเช่น: wg21.link/p0062และwg21.link/n4455 ขอบคุณ Andy ที่นำสิ่งเหล่านี้มาให้ฉัน
Richard Hodges

38

การเรียนการสอนแบบadd DWORD PTR [rbp-4], 1CISC นั้นไม่ยุ่งยากซับซ้อนนัก

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

AGENT 1          AGENT 2

load X              
inc C
                 load X
                 inc C
                 store X
store X

X จะเพิ่มขึ้นเพียงครั้งเดียว


7
@LeoHeinsaar เพื่อให้เป็นไปตามนั้นชิปหน่วยความจำแต่ละอันจะต้องใช้ Arithmetic Logic Unit (ALU) ของตัวเอง มันจะมีผลกำหนดให้ชิปหน่วยความจำแต่ละเป็นหน่วยประมวลผล
Richard Hodges

6
@LeoHeinsaar: คำแนะนำหน่วยความจำปลายทางคือการดำเนินการอ่าน - แก้ไข - เขียน ไม่มีการลงทะเบียนสถาปัตยกรรม แต่ CPU ต้องเก็บข้อมูลภายในขณะที่ส่งผ่าน ALU ไฟล์การลงทะเบียนจริงเป็นเพียงส่วนเล็ก ๆ ของการจัดเก็บข้อมูลภายในแม้แต่ CPU ที่ง่ายที่สุดโดยมีสลักเอาท์พุทของสเตจเดียวเป็นอินพุตสำหรับสเตจอื่น ฯลฯ
Peter Cordes

@PeterCordes ความคิดเห็นของคุณเป็นคำตอบที่ฉันต้องการ คำตอบของมาร์กาเร็ตทำให้ฉันสงสัยว่าจะต้องเข้าไปข้างใน
Leo Heinsaar

เปลี่ยนความคิดเห็นนั้นเป็นคำตอบแบบเต็มรวมถึงการระบุส่วน C ++ ของคำถาม
Peter Cordes

1
@ PeterCordes ขอบคุณรายละเอียดมากและในทุกจุด เห็นได้ชัดว่าเป็นการแข่งขันของข้อมูลและพฤติกรรมที่ไม่ได้กำหนดตามมาตรฐาน C ++ ฉันแค่อยากรู้ว่าในกรณีที่รหัสที่สร้างขึ้นเป็นสิ่งที่ฉันโพสต์คนหนึ่งอาจสันนิษฐานได้ว่าอาจเป็นอะตอมมิก ฯลฯ เป็นต้น คู่มืออย่างชัดเจนกำหนดatomicityเกี่ยวกับการดำเนินการของหน่วยความจำและไม่แบ่งแยกการเรียนการสอนตามที่ฉันสันนิษฐานว่า: "การดำเนินการที่ถูกล็อคเป็น atomic ที่เกี่ยวกับการดำเนินงานของหน่วยความจำอื่น ๆ และเหตุการณ์ภายนอกที่มองเห็นทั้งหมด"
Leo Heinsaar

11

คำสั่งเพิ่มไม่ใช่อะตอมมิก มันอ้างถึงหน่วยความจำและแกนประมวลผลสองคอร์อาจมีแคชในตัวที่แตกต่างกันของหน่วยความจำนั้น

IIRC ตัวแปรอะตอมมิกของคำสั่งการเพิ่มเรียกว่าlock xadd


3
lock xaddใช้ C ++ std :: atomic fetch_addส่งคืนค่าเก่า หากคุณไม่ต้องการคอมไพเลอร์จะใช้คำแนะนำปลายทางหน่วยความจำปกติพร้อมlockคำนำหน้า หรือlock add lock inc
Peter Cordes

1
add [mem], 1จะยังไม่เป็น atomic ในเครื่อง SMP ที่ไม่มีแคชดูความเห็นของฉันสำหรับคำตอบอื่น ๆ
Peter Cordes

ดูคำตอบของฉันสำหรับรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการที่ไม่ใช่อะตอม ในตอนท้ายของคำตอบของฉันในคำถามที่เกี่ยวข้องนี้
Peter Cordes

10

เนื่องจากบรรทัดที่ 5 ซึ่งสอดคล้องกับ num ++ เป็นคำสั่งเดียวเราสามารถสรุปได้ว่า num ++ เป็นอะตอมมิกในกรณีนี้หรือไม่

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

นอกจากนี้ความคิดของคุณว่าคำสั่งการประกอบหนึ่งหมายถึงการดำเนินการเป็นปรมาณูก็ผิดเช่นกัน สิ่งนี้addจะไม่เป็น atomic ในระบบ multi-CPU แม้ในสถาปัตยกรรม x86


9

แม้ว่าคอมไพเลอร์ของคุณจะปล่อยสิ่งนี้เป็นการดำเนินการแบบปรมาณูเสมอการเข้าถึงnumจากเธรดอื่น ๆ พร้อมกันนั้นจะเป็นการรวบรวมข้อมูลตามมาตรฐาน C ++ 11 และ C ++ 14 และโปรแกรมจะมีพฤติกรรมที่ไม่ได้กำหนดไว้

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

int main()
{
  std::unique_ptr<std::vector<int>> vec;
  int ready = 0;
  std::thread t{[&]
    {
       while (!ready);
       // use "vec" here
    });
  vec.reset(new std::vector<int>());
  ++ready;
  t.join();
}

แม้ว่าเราจะมองโลกในแง่ดีว่า++ready"ปรมาณู" และคอมไพเลอร์สร้างลูปการตรวจสอบตามที่ต้องการ (อย่างที่ฉันบอกว่ามันคือ UB ดังนั้นคอมไพเลอร์จึงมีอิสระที่จะลบออกแทนที่ด้วยลูปไม่สิ้นสุด ฯลฯ ) คอมไพเลอร์อาจยังคงย้ายการกำหนดตัวชี้หรือยิ่งกว่านั้นการกำหนดค่าเริ่มต้นของvectorไปยังจุดหลังจากการดำเนินการเพิ่มขึ้นทำให้เกิดความสับสนวุ่นวายในเธรดใหม่ ในทางปฏิบัติฉันจะไม่แปลกใจเลยถ้าคอมไพเลอร์ออปติไมซ์ทำการลบreadyตัวแปรและลูปการตรวจสอบอย่างสมบูรณ์เนื่องจากสิ่งนี้จะไม่ส่งผลต่อพฤติกรรมที่สังเกตได้ภายใต้กฎภาษา (ตรงข้ามกับความหวังส่วนตัวของคุณ)

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

สุดท้ายแม้ถ้าคุณไม่ได้สนใจเกี่ยวกับการพกพาและคอมไพเลอร์ของคุณได้ดีอย่างน่าอัศจรรย์ซีพียูที่คุณใช้เป็นอย่างมากที่มีแนวโน้มของประเภท CISC superscalar และจะทำลายลงคำแนะนำลงในไมโคร Ops, การสั่งซื้อและ / หรือการพิจารณาดำเนินการกับพวกเขา ในขอบเขตที่ จำกัด โดยการซิงโครไนซ์แบบดั้งเดิมเช่น (บน Intel) LOCKคำนำหน้าหรือรั้วหน่วยความจำเพื่อเพิ่มการดำเนินงานสูงสุดต่อวินาที

หากต้องการสรุปสั้น ๆ ความรับผิดชอบตามธรรมชาติของการเขียนโปรแกรมที่ปลอดภัยต่อเธรดคือ:

  1. หน้าที่ของคุณคือการเขียนรหัสที่มีพฤติกรรมที่ชัดเจนภายใต้กฎของภาษา (และโดยเฉพาะอย่างยิ่งในรูปแบบหน่วยความจำมาตรฐานภาษา)
  2. หน้าที่ของคอมไพเลอร์ของคุณคือการสร้างรหัสเครื่องซึ่งมีพฤติกรรมที่ชัดเจน (สังเกตได้) เหมือนกันภายใต้โมเดลหน่วยความจำของสถาปัตยกรรมเป้าหมาย
  3. หน้าที่ของ CPU คือการเรียกใช้โค้ดนี้เพื่อให้พฤติกรรมที่สังเกตได้เข้ากันได้กับโมเดลหน่วยความจำของสถาปัตยกรรมของตัวเอง

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

PS: ตัวอย่างที่เขียนอย่างถูกต้อง:

int main()
{
  std::unique_ptr<std::vector<int>> vec;
  std::atomic<int> ready{0}; // NOTE the use of the std::atomic template
  std::thread t{[&]
    {
       while (!ready);
       // use "vec" here
    });
  vec.reset(new std::vector<int>());
  ++ready;
  t.join();
}

สิ่งนี้ปลอดภัยเพราะ:

  1. การตรวจสอบของreadyไม่สามารถปรับให้เหมาะสมตามกฎภาษา
  2. ++ready เกิดขึ้นก่อนที่จะตรวจสอบที่เห็นreadyเป็นไม่เป็นศูนย์และการดำเนินงานอื่น ๆ ที่ไม่สามารถจัดลำดับใหม่รอบดำเนินการเหล่านี้ นี่เป็นเพราะ++readyและการตรวจสอบนั้นมีความสอดคล้องกันตามลำดับซึ่งเป็นอีกคำหนึ่งที่อธิบายไว้ในโมเดลหน่วยความจำ C ++ และห้ามการจัดเรียงใหม่นี้โดยเฉพาะ ดังนั้นคอมไพเลอร์ไม่ต้องสั่งซื้อใหม่คำแนะนำและยังต้องบอก CPU ที่ว่ามันจะต้องไม่เช่นเลื่อนการเขียนเพื่อการหลังจากที่เพิ่มขึ้นของvec ความสอดคล้องกันอย่างต่อเนื่องคือการรับประกันที่แข็งแกร่งที่สุดเกี่ยวกับอะตอมมิกส์ในมาตรฐานภาษา มีการรับประกันน้อยกว่า (และถูกกว่าในทางทฤษฎี) เช่นผ่านวิธีการอื่น ๆ ของreadystd::atomic<T>แต่สิ่งเหล่านี้มีไว้สำหรับผู้เชี่ยวชาญเท่านั้นและอาจไม่ได้รับการปรับให้เหมาะสมโดยนักพัฒนาคอมไพเลอร์เพราะพวกเขาไม่ค่อยได้ใช้

1
ถ้าคอมไพเลอร์ไม่สามารถมองเห็นการใช้งานทั้งหมดของreadyก็อาจจะรวบรวมเป็นสิ่งที่มากขึ้นเช่นwhile (!ready); if(!ready) { while(true); }Upvoted: ส่วนสำคัญของ std :: atomic กำลังเปลี่ยนซีแมนทิกส์เพื่อรับการดัดแปลงแบบอะซิงโครนัสที่จุดใดก็ได้ โดยปกติแล้ว UB จะเป็นสิ่งที่ทำให้คอมไพเลอร์ยกโหลดและเก็บของออกจากลูป
Peter Cordes

9

บนเครื่อง x86 แบบ single-core การaddเรียนการสอนโดยทั่วไปจะมีอะตอมที่เกี่ยวกับรหัสอื่น ๆ บน CPU 1 ขัดจังหวะไม่สามารถแยกคำสั่งเดียวลงกลาง

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

ระบบ x86 ที่ทันสมัยเป็นแบบมัลติคอร์ดังนั้นกรณีพิเศษตัวประมวลผลเดียวจึงใช้ไม่ได้

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

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


เชิงอรรถ 1: มีlockคำนำหน้าอยู่แม้ในต้นฉบับ 8086 เพราะอุปกรณ์ I / O ทำงานพร้อมกับ CPU ไดรเวอร์ในระบบแบบแกนเดียวจำเป็นต้องlock addเพิ่มค่าในหน่วยความจำอุปกรณ์แบบอะตอมมิกหากอุปกรณ์สามารถปรับเปลี่ยนได้หรือเกี่ยวข้องกับการเข้าถึง DMA


มันไม่ได้เป็นอะตอมโดยทั่วไป: เธรดอื่นสามารถอัปเดตตัวแปรเดียวกันในเวลาเดียวกันและมีการอัปเดตเพียงครั้งเดียวเท่านั้น
fuz

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

1
@FUZxxl: คำตอบที่สี่และห้าของฉันคืออะไร?
supercat

1
@supercat คำตอบของคุณทำให้เข้าใจผิดมากเพราะมันจะพิจารณาเฉพาะกรณีที่หายากในปัจจุบันของแกนเดียวและให้ความรู้สึกผิด OP ของการรักษาความปลอดภัย นั่นเป็นเหตุผลที่ฉันแสดงความคิดเห็นเพื่อพิจารณากรณีแบบ multi-core เช่นกัน
fuz

1
@FUZxxl: ฉันได้ทำการแก้ไขเพื่อกำจัดความสับสนที่อาจเกิดขึ้นสำหรับผู้อ่านที่ไม่ได้สังเกตว่านี่ไม่ได้พูดถึง CPU แบบมัลติคอร์ที่ทันสมัยตามปกติ (และยังมีความเฉพาะเจาะจงมากขึ้นเกี่ยวกับบางสิ่งที่ supercat ไม่แน่ใจ) BTW ทุกอย่างในคำตอบนี้มีอยู่แล้วในเหมืองยกเว้นประโยคสุดท้ายเกี่ยวกับวิธีที่แพลตฟอร์มที่การอ่าน - แก้ไข - เขียนเป็นปรมาณู "ฟรี" เป็นของหายาก
Peter Cordes

7

ย้อนกลับไปในวันที่คอมพิวเตอร์ x86 มี CPU หนึ่งตัวการใช้คำสั่งเดียวทำให้มั่นใจได้ว่าอินเตอร์รัปต์จะไม่แยกการอ่าน / แก้ไข / เขียนและหากหน่วยความจำจะไม่ถูกใช้เป็นบัฟเฟอร์ DMA ด้วยเช่นกันมันเป็นอะตอมจริง C ++ ไม่ได้กล่าวถึงเธรดในมาตรฐานดังนั้นจึงไม่ได้รับการแก้ไข)

เมื่อมันยากที่จะมีหน่วยประมวลผลคู่ (เช่นซ็อกเก็ต Pentium Pro) บนเดสก์ท็อปลูกค้าฉันใช้สิ่งนี้อย่างมีประสิทธิภาพเพื่อหลีกเลี่ยงคำนำหน้า LOCK บนเครื่องแกนเดียวและปรับปรุงประสิทธิภาพ

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

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


1
ขัดจังหวะยังคงทำไม่ได้การดำเนินงาน RMW แยกดังนั้นพวกเขาจะยังคงประสานหัวข้อเดียวกับตัวจัดการสัญญาณที่วิ่งในหัวข้อเดียวกัน แน่นอนมันใช้งานได้เฉพาะในกรณีที่ asm ใช้คำสั่งเดียวไม่แยกโหลด / แก้ไข / เก็บ C ++ 11 สามารถเปิดเผยการทำงานของฮาร์ดแวร์นี้ได้ แต่มันไม่ได้ (อาจเป็นเพราะมันมีประโยชน์จริงๆในเมล็ด Uniprocessor เพื่อซิงโครไนซ์กับตัวจัดการอินเตอร์รัปต์ไม่ใช่ในพื้นที่ผู้ใช้ที่มีตัวจัดการสัญญาณ) สถาปัตยกรรมยังไม่มีคำแนะนำหน่วยความจำปลายทางอ่าน - แก้ไข - เขียน ถึงกระนั้นมันก็สามารถรวบรวมเช่นอะตอม RMW ผ่อนคลายใน non-x86
Peter Cordes

แม้ว่าในขณะที่ฉันจำได้ แต่การใช้คำนำหน้า Lock ไม่ได้มีราคาแพงจนน่าประหลาดใจจนกระทั่งซูเปอร์สโตร์มาถึง ดังนั้นจึงไม่มีเหตุผลที่จะสังเกตว่ามันช้าลงในรหัสที่สำคัญใน 486 แม้ว่ามันจะไม่จำเป็นสำหรับโปรแกรมนั้นก็ตาม
JDługosz

ใช่ขอโทษ! ฉันไม่ได้อ่านอย่างถี่ถ้วน ฉันเห็นจุดเริ่มต้นของย่อหน้าพร้อมกับปลาเฮอริ่งแดงเกี่ยวกับการถอดรหัสเป็น uops และไม่ได้อ่านจบเพื่อดูสิ่งที่คุณพูดจริง ๆ Re: 486: ฉันคิดว่าฉันได้อ่านว่า SMP แรกสุดนั้นเป็น Compaq 386 บางชนิด แต่ความหมายของการสั่งหน่วยความจำไม่เหมือนกับตอนที่ x86 ISA พูด คู่มือ x86 ปัจจุบันอาจพูดถึง SMP 486 แน่นอนว่ามันไม่ได้เป็นเรื่องธรรมดาแม้แต่ใน HPC (กลุ่ม Beowulf) จนถึง PPro / Athlon XP วัน แต่ฉันคิดว่า
Peter Cordes

1
@PeterCordes ตกลง แน่นอนว่าสมมติว่าไม่มีผู้สังเกตการณ์ DMA / อุปกรณ์ - ไม่พอดีในพื้นที่แสดงความคิดเห็นเพื่อรวมสิ่งนั้นไว้ด้วย ขอบคุณJDługoszสำหรับการเพิ่มยอดเยี่ยม (คำตอบและความคิดเห็น) เสร็จสิ้นการอภิปราย
Leo Heinsaar

3
@Leo: จุดสำคัญหนึ่งที่ไม่ได้กล่าวถึง: ซีพียูนอกสั่งทำสิ่งต่าง ๆ ภายใน แต่กฎทองก็คือสำหรับแกนเดียวพวกเขารักษาภาพลวงตาของคำสั่งที่ทำงานทีละครั้งตามลำดับ (และสิ่งนี้รวมถึงการขัดจังหวะที่ทำให้เกิดการสลับบริบท) ค่าอาจถูกเก็บไว้ในหน่วยความจำไฟฟ้าที่ไม่เป็นระเบียบ แต่แกนเดียวที่ทุกอย่างกำลังทำงานอยู่นั้นคอยติดตามการเรียงลำดับใหม่ทั้งหมดที่มันทำเพื่อรักษาภาพลวงตา นี่คือเหตุผลที่คุณไม่จำเป็นต้องมีกำแพงกั้นหน่วยความจำสำหรับ asm ที่เทียบเท่าa = 1; b = a;กับโหลด 1 ที่คุณเพิ่งเก็บ
Peter Cordes

4

ไม่ https://www.youtube.com/watch?v=31g0YE61PLQ (นั่นเป็นเพียงลิงก์ไปยังฉาก "ไม่" จาก "The Office")

คุณเห็นด้วยหรือไม่ว่านี่จะเป็นผลลัพธ์ที่เป็นไปได้สำหรับโปรแกรม:

ตัวอย่างผลลัพธ์:

100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100

ถ้าเป็นเช่นนั้นคอมไพเลอร์มีอิสระที่จะสร้างเอาต์พุตที่เป็นไปได้เพียงอย่างเดียวสำหรับโปรแกรม เช่น main () ที่เพิ่งผ่าน 100s

นี่คือกฎ "as-if"

และไม่คำนึงถึงการส่งออกที่คุณสามารถคิดของการประสานด้ายทางเดียวกัน - ถ้าด้ายไม่num++; num--;และด้าย B อ่านnumซ้ำแล้ว interleaving ที่ถูกต้องเป็นไปได้คือด้าย B ไม่เคยอ่านระหว่างและnum++ num--เนื่องจาก interleaving นั้นถูกต้องคอมไพเลอร์จึงมีอิสระที่จะทำให้interleaving เป็นไปได้เท่านั้น และเพียงแค่ลบ incr / decr ทั้งหมด

มีนัยยะที่น่าสนใจอยู่ที่นี่:

while (working())
    progress++;  // atomic, global

(เช่นจินตนาการว่าเธรดอื่น ๆ อัพเดต UI ของแถบความคืบหน้าตามprogress)

คอมไพเลอร์สามารถเปลี่ยนสิ่งนี้เป็น:

int local = 0;
while (working())
    local++;

progress += local;

อาจเป็นสิ่งที่ถูกต้อง แต่อาจไม่ใช่สิ่งที่โปรแกรมเมอร์หวังไว้ :-(

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

และแม้ว่าจะprogressมีความผันผวนเช่นนี้ก็ยังคงใช้ได้:

int local = 0;
while (working())
    local++;

while (local--)
    progress++;

: - /


คำตอบนี้ดูเหมือนจะตอบคำถามด้านที่ Richard และฉันไตร่ตรองเท่านั้น เราได้รับการแก้ไขในที่สุดมันจะเปิดออกใช่ว่า c ++ มาตรฐานไม่อนุญาตให้มีการควบรวมของการดำเนินงานเกี่ยวกับการที่ไม่ใช่volatileวัตถุอะตอมเมื่อมันไม่ได้ทำลายกฎระเบียบอื่น ๆ เอกสารการอภิปรายมาตรฐานสองฉบับกล่าวถึงสิ่งนี้อย่างสมบูรณ์ (ลิงก์ในความคิดเห็นของริชาร์ด ) โดยใช้ตัวอย่างความคืบหน้าเคาน์เตอร์เดียวกัน ดังนั้นจึงเป็นปัญหาคุณภาพของการนำไปใช้จนกว่า C ++ จะทำให้เป็นมาตรฐานในการป้องกัน
Peter Cordes

ใช่ "ไม่" ของฉันเป็นคำตอบจริงๆสำหรับเหตุผลทั้งหมด หากคำถามเป็นเพียง "สามารถ num ++ เป็นอะตอมในคอมไพเลอร์ / การใช้งาน" คำตอบคือแน่นอน ตัวอย่างเช่นคอมไพเลอร์สามารถตัดสินใจที่จะเพิ่มlockในทุกการดำเนินการ หรือคอมไพเลอร์ชุดรวมตัวประมวลผลเดียวที่ไม่มีการเรียงลำดับใหม่ (เช่น "วันเฒ่าที่ดี") ทุกอย่างเป็นอะตอม แต่ประเด็นคืออะไร คุณไม่สามารถไว้ใจมันได้ หากคุณไม่ทราบว่าเป็นระบบที่คุณกำลังเขียน (แล้วถึงแม้จะดีกว่าที่อะตอม <int> เพิ่มไม่มีปฏิบัติการพิเศษในระบบที่ดังนั้นคุณยังควรเขียนรหัสมาตรฐาน ... .)
tony

1
โปรดทราบว่าAnd just remove the incr/decr entirely.ไม่ถูกต้องนัก numมันยังคงได้รับและปล่อยให้เป็นอิสระในการดำเนินงาน ใน x86 num++;num--สามารถรวบรวมเป็นเพียง MFENCE แต่ไม่ได้ทำอะไรเลย (เว้นแต่การวิเคราะห์ทั้งโปรแกรมของคอมไพเลอร์สามารถพิสูจน์ได้ว่าไม่มีอะไร sychronizes กับการปรับเปลี่ยนของ NUM และว่ามันไม่สำคัญว่าบางร้านจากก่อนที่จะล่าช้าจนกว่าหลังจากโหลดจากหลังจากนั้น) เช่นถ้าเป็นปลดล็อคและอีกครั้ง - ล็อค - ใช้งานได้ทันทีคุณยังคงมีสองส่วนที่สำคัญแยกกัน (อาจจะใช้ mo_relaxed) ไม่ใช่หนึ่งชิ้นใหญ่
Peter Cordes

@ PeterCordes ah ใช่เห็นด้วย
โทนี

2

ใช่ แต่...

อะตอมไม่ใช่สิ่งที่คุณตั้งใจจะพูด คุณอาจถามสิ่งผิดปกติ

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

มันปลอดภัยไหม?

นี่เป็นคำถามที่แตกต่างและอย่างน้อยสองเหตุผลที่ดีที่จะตอบด้วยคำว่า"ไม่!" .

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

ประการที่สองมีการสั่งซื้อหน่วยความจำหรือมีคำพูดที่แตกต่างกันเกิดขึ้นก่อนการรับประกัน สิ่งที่สำคัญที่สุดเกี่ยวกับคำแนะนำของอะตอมไม่มากว่าพวกเขาเป็นอะตอม มันสั่ง

คุณมีความเป็นไปได้ที่จะบังคับใช้การรับประกันว่าทุกอย่างที่เกิดขึ้นกับหน่วยความจำที่ชาญฉลาดนั้นจะเกิดขึ้นในการรับประกันที่กำหนดไว้อย่างดีซึ่งคุณมีการรับประกัน "เกิดขึ้นก่อน" คำสั่งนี้อาจจะเป็น "ผ่อนคลาย" (อ่านเป็น: ไม่มีเลย) หรือเข้มงวดเท่าที่คุณต้องการ

ตัวอย่างเช่นคุณสามารถตั้งค่าตัวชี้ไปยังบล็อกข้อมูลบางส่วน (เช่นผลลัพธ์ของการคำนวณบางอย่าง) จากนั้นปล่อยธง "data is ready" แบบอะตอม ตอนนี้ใครก็ตามที่ได้รับธงนี้จะถูกนำไปสู่การคิดว่าตัวชี้นั้นถูกต้อง และแน่นอนมันจะเป็นตัวชี้ที่ถูกต้องเสมอไม่มีอะไรแตกต่าง นั่นเป็นเพราะการเขียนไปยังตัวชี้เกิดขึ้นก่อนที่การปฏิบัติการปรมาณู


2
โหลดและการจัดเก็บแต่ละอะตอมแยกต่างหาก แต่การดำเนินการปรับเปลี่ยนอ่านเขียนทั้งเป็นทั้งเป็นมั่นเหมาะไม่อะตอม แคชมีความสอดคล้องกันดังนั้นจึงไม่สามารถเก็บสำเนาที่ขัดแย้งกันของบรรทัดเดียวกันได้ ( en.wikipedia.org/wiki/MESI_protocol ) คอร์อื่นไม่สามารถมีสำเนาที่อ่านได้อย่างเดียวในขณะที่คอร์นี้มีอยู่ในสถานะดัดแปลง สิ่งที่ทำให้ไม่ใช่ปรมาณูคือแกนทำ RMW อาจสูญเสียความเป็นเจ้าของของบรรทัดแคชระหว่างการโหลดและการจัดเก็บ
Peter Cordes

2
นอกจากนี้ไม่บรรทัดแคชทั้งหมดจะไม่ถูกถ่ายโอนไปทั่วอะตอม ดูคำตอบนี้ซึ่งมีการทดลองแสดงให้เห็นว่า multi-socket Opteron ทำให้ 16B SSE จัดเก็บแบบไม่ปรมาณูโดยการโอนสายแคชในกลุ่ม 8B ที่มี hypertransport แม้ว่าพวกเขาจะเป็น atomic สำหรับ CPU แบบซ็อกเก็ตชนิดเดียวกัน (เนื่องจากโหลด / ฮาร์ดแวร์ร้านค้ามีเส้นทาง 16B ไปยังแคช L1) x86 รับประกันได้ว่าอะตอมมิกซิตี้สำหรับโหลดแยกหรือเก็บสูงสุด 8B
Peter Cordes

การจัดแนวซ้ายไปยังคอมไพเลอร์ไม่ได้หมายความว่าหน่วยความจำจะถูกจัดตำแหน่งในขอบเขต 4 ไบต์ คอมไพเลอร์สามารถมีตัวเลือกหรือ pragmas เพื่อเปลี่ยนขอบเขตการจัดตำแหน่ง สิ่งนี้มีประโยชน์เช่นสำหรับการดำเนินการกับข้อมูลที่อัดแน่นในสตรีมเครือข่าย
Dmitry Rubanovich

2
ไม่มีอะไรอื่น จำนวนเต็มกับการจัดเก็บโดยอัตโนมัติซึ่งไม่ได้เป็นส่วนหนึ่งของโครงสร้างดังแสดงในตัวอย่างจะอย่างในเชิงบวกจะสอดคล้องอย่างถูกต้อง การอ้างสิ่งที่แตกต่างเป็นเพียงเรื่องไร้สาระ เส้นแคชและ POD ทั้งหมดเป็น PoT (power-of-two) ขนาดและจัดตำแหน่ง - บนสถาปัตยกรรมที่ไม่ใช่ภาพลวงตาใด ๆ ในโลก คณิตศาสตร์ระบุว่า PoT ที่มีการจัดตำแหน่งอย่างถูกต้องเหมาะสมพอดีกับ PoT อื่น ๆ ที่มีขนาดเท่ากันหรือมากกว่านั้น คำสั่งของฉันจึงถูกต้อง
เดมอน

1
@Damon ตัวอย่างที่ให้ไว้ในคำถามไม่ได้พูดถึง struct แต่ไม่ได้ จำกัด คำถามเพียงแค่สถานการณ์ที่จำนวนเต็มไม่ใช่ส่วนของ struct PODs ส่วนใหญ่สามารถมีขนาด PoT และไม่สามารถจัดตำแหน่ง PoT ได้ ลองดูที่คำตอบนี้ตัวอย่างไวยากรณ์: stackoverflow.com/a/11772340/1219722 ดังนั้นจึงแทบจะไม่เป็น "ความซับซ้อน" เนื่องจาก POD ที่ประกาศในลักษณะนี้ใช้ในโค้ดเครือข่ายค่อนข้างน้อยในรหัสชีวิตจริง
Dmitry Rubanovich

2

ว่าการส่งออกคอมไพเลอร์เดียวบนสถาปัตยกรรมเฉพาะ CPU ที่มีการเพิ่มประสิทธิภาพการปิดการใช้งาน (ตั้งแต่ GCC ไม่ได้รวบรวม++ไปaddเมื่อการเพิ่มประสิทธิภาพในการเป็นตัวอย่างรวดเร็วและสกปรก ) ดูเหมือนว่าจะบ่งบอกถึงการเพิ่มวิธีนี้คืออะตอมไม่ได้หมายความว่านี้คือตามมาตรฐาน ( ที่คุณจะทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดเมื่อพยายามที่จะเข้าถึงnumในหัวข้อ) และเป็นสิ่งที่ผิดนะเพราะaddเป็นไม่ได้อะตอมใน x86

โปรดทราบว่า atomics (ใช้lockคำนำหน้าคำสั่ง) ค่อนข้างหนักใน x86 ( ดูคำตอบที่เกี่ยวข้องนี้ ) แต่ก็ยังน้อยกว่า mutex อย่างน่าทึ่งซึ่งไม่เหมาะสมในกรณีการใช้งานนี้

ผลการต่อไปนี้จะนำมาจากเสียงดังกราว ++ 3.8 -Osเมื่อรวบรวมกับ

การเพิ่ม int โดยการอ้างอิงวิธี "ปกติ":

void inc(int& x)
{
    ++x;
}

รวบรวมนี้เป็น:

inc(int&):
    incl    (%rdi)
    retq

การเพิ่ม int ที่ส่งผ่านโดยการอ้างอิงวิธีอะตอมมิก:

#include <atomic>

void inc(std::atomic<int>& x)
{
    ++x;
}

ตัวอย่างนี้ซึ่งไม่ซับซ้อนกว่าวิธีปกติมากเพียงแค่lockนำส่วนเสริมที่เพิ่มเข้าไปในinclคำสั่ง - แต่ต้องระวังตามที่ระบุไว้ก่อนหน้านี้ว่าไม่ถูก เพียงเพราะการชุมนุมดูสั้นไม่ได้หมายความว่ามันเร็ว

inc(std::atomic<int>&):
    lock            incl    (%rdi)
    retq

-2

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


-3

ลองรวบรวมรหัสเดียวกันบนเครื่องที่ไม่ใช่ x86 และคุณจะเห็นผลการประกอบที่แตกต่างกันอย่างรวดเร็ว

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

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

เหตุผลที่เรามีstd::atomic<int>และต่อ ๆ ไปคือเมื่อคุณทำงานกับสถาปัตยกรรมที่ไม่ได้รับประกันว่าอะตอมมิกของการคำนวณพื้นฐานคุณมีกลไกที่จะบังคับให้คอมไพเลอร์สร้างรหัสอะตอมมิก


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

8
มันไม่ได้เป็นอะตอมใน x86 เช่นกัน มันเป็นแบบ single-core-safe แต่ถ้ามีหลายคอร์ (และมี) ก็ไม่ได้เป็นอะตอมเลย
แฮโรลด์

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

@Slava @Harold @ShadowRanger ฉันได้อัพเดตคำตอบแล้ว addเป็นปรมาณู แต่ฉันได้ระบุไว้อย่างชัดเจนว่านั่นไม่ได้หมายความว่ารหัสนั้นปลอดภัยสำหรับสภาพการแข่งขันเนื่องจากการเปลี่ยนแปลงไม่สามารถมองเห็นได้ทั่วโลกในทันที
Xirema

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