สารระเหยมีราคาแพงหรือไม่?


111

หลังจากอ่านตำรา JSR-133 สำหรับนักเขียนคอมไพเลอร์เกี่ยวกับการใช้สารระเหยโดยเฉพาะส่วน "การโต้ตอบกับคำแนะนำเกี่ยวกับอะตอม" ฉันคิดว่าการอ่านตัวแปรระเหยโดยไม่ต้องอัปเดตจำเป็นต้องมี LoadLoad หรือ LoadStore barrier ถัดไปในหน้าฉันเห็นว่า LoadLoad และ LoadStore นั้นไม่มีประสิทธิภาพบนซีพียู X86 นี่หมายความว่าการดำเนินการอ่านแบบระเหยสามารถทำได้โดยไม่ต้องมีการยกเลิกแคชอย่างชัดเจนบน x86 และเร็วเท่ากับการอ่านตัวแปรปกติ (โดยไม่คำนึงถึงข้อ จำกัด ในการจัดลำดับใหม่ของการระเหย)?

ฉันเชื่อว่าฉันเข้าใจสิ่งนี้ไม่ถูกต้อง มีใครสนใจที่จะสอนฉันได้ไหม

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

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


1
คุณสามารถอ่านการแก้ไขของฉันเกี่ยวกับการกำหนดค่าที่มี CPU หลายตัวที่คุณอ้างถึง อาจเกิดขึ้นได้ในระบบหลาย CPU สำหรับการอ้างอิงที่มีอายุสั้นจะไม่มีการอ่าน / เขียนไปยังหน่วยความจำหลักเพียงครั้งเดียวอีกต่อไป
John Vint

2
การอ่านค่าระเหยนั้นไม่แพง ต้นทุนหลักคือวิธีป้องกันการเพิ่มประสิทธิภาพ ในทางปฏิบัตินั้นค่าใช้จ่ายโดยเฉลี่ยไม่สูงมากเช่นกันเว้นแต่จะใช้ความผันผวนในวง จำกัด
ปฏิเสธไม่ได้

2
บทความเกี่ยวกับ infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) นี้อาจสนใจคุณเช่นกันโดยจะแสดงผลของความผันผวนและซิงโครไนซ์กับโค้ดที่สร้างขึ้นสำหรับสถาปัตยกรรมที่แตกต่างกัน นี่เป็นกรณีหนึ่งที่ jvm สามารถทำงานได้ดีกว่าคอมไพเลอร์ล่วงหน้าเนื่องจากมันรู้ว่ากำลังทำงานบนระบบยูนิโพรเซสเซอร์หรือไม่และสามารถข้ามอุปสรรคของหน่วยความจำบางอย่างได้
Jörn Horstmann

คำตอบ:


123

ใน Intel การอ่านค่าระเหยที่ไม่ได้โต้แย้งนั้นค่อนข้างถูก หากเราพิจารณากรณีง่ายๆดังต่อไปนี้:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

การใช้ความสามารถของ Java 7 ในการพิมพ์รหัสแอสเซมบลีวิธีการรันจะมีลักษณะดังนี้:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

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

ถ้าเราทำให้ตัวแปร l ระเหยการประกอบผลลัพธ์จะแตกต่างกัน

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

ในกรณีนี้การอ้างอิงทั้งสองแบบ getstatic ไปยังตัวแปร l เกี่ยวข้องกับการโหลดจากหน่วยความจำกล่าวคือไม่สามารถเก็บค่าไว้ในรีจิสเตอร์ในการอ่านค่าระเหยหลาย ๆ เพื่อให้แน่ใจว่ามีการอ่านค่าอะตอมค่าจะถูกอ่านจากหน่วยความจำหลักไปยังการลงทะเบียน MMX movsd 0x6fb7b2f0(%ebp),%xmm0ทำให้การดำเนินการอ่านเป็นคำสั่งเดียว (จากตัวอย่างก่อนหน้านี้เราเห็นว่าปกติค่า 64 บิตจะต้องอ่าน 32 บิตสองครั้งในระบบ 32 บิต)

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


หมายเหตุด้านข้าง java 6 มีความสามารถในการแสดงแอสเซมบลีเหมือนกัน (เป็นฮอตสปอตที่ทำ)
ที่สุด

+1 ใน JDK5 volatile ไม่สามารถจัดเรียงใหม่เกี่ยวกับการอ่าน / เขียนใด ๆ (ซึ่งแก้ไขการล็อกการตรวจสอบอีกครั้งเป็นต้น) นั่นหมายความว่ามันจะส่งผลต่อการจัดการฟิลด์ที่ไม่ลบเลือนด้วยหรือไม่? มันน่าสนใจที่จะผสมผสานการเข้าถึงฟิลด์ที่ผันผวนและไม่ลบเลือน
ewernli

@evemli คุณต้องระวังฉันทำคำพูดนี้ด้วยตัวเองครั้งเดียว แต่พบว่าไม่ถูกต้อง มีขอบเคส Java Memory Model ช่วยให้ความหมายของ roach motel เมื่อร้านค้าสามารถสั่งซื้อใหม่ได้ก่อนร้านค้าที่ผันผวน หากคุณหยิบสิ่งนี้มาจากบทความ Brian Goetz บนไซต์ IBM คุณควรกล่าวถึงว่าบทความนี้ช่วยลดความซับซ้อนของข้อกำหนด JMM
Michael Barker

20

โดยทั่วไปแล้วในโปรเซสเซอร์สมัยใหม่ส่วนใหญ่โหลดที่ระเหยได้เปรียบได้กับโหลดปกติ ร้านค้าที่ระเหยได้จะอยู่ที่ประมาณ 1/3 ของเวลาในการเข้า / ออกมอนดิออร์ สิ่งนี้จะเห็นได้ในระบบที่แคชสอดคล้องกัน

เพื่อตอบคำถามของ OP การเขียนแบบระเหยมีราคาแพงในขณะที่การอ่านมักจะไม่

หมายความว่าการดำเนินการอ่านแบบระเหยสามารถทำได้โดยไม่ต้องมีการยกเลิกแคชอย่างชัดเจนบน x86 และเร็วเท่ากับการอ่านตัวแปรปกติ

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

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

แก้ไขเพื่อตอบการแก้ไขของ OP:

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

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


5
AtomicReference เป็นเพียงเครื่องห่อหุ้มไปยังเขตข้อมูลที่มีความผันผวนพร้อมด้วยฟังก์ชันเนทีฟที่เพิ่มเข้ามาซึ่งมีฟังก์ชันเพิ่มเติมเช่น getAndSet, CompareAndSet เป็นต้นดังนั้นจากมุมมองประสิทธิภาพการใช้งานจะมีประโยชน์หากคุณต้องการฟังก์ชันเพิ่มเติม แต่ฉันสงสัยว่าทำไมคุณถึงอ้างถึง OS ที่นี่? ฟังก์ชันนี้ถูกนำไปใช้ใน opcodes ของ CPU โดยตรง และนี่หมายความว่าในระบบโปรเซสเซอร์หลายตัวโดยที่ซีพียูตัวหนึ่งไม่มีความรู้เกี่ยวกับเนื้อหาแคชของซีพียูตัวอื่นที่โวลไทล์ช้ากว่าเนื่องจากซีพียูต้องโดนหน่วยความจำหลักเสมอ?
Daniel

ใช่แล้วฉันพลาดพูดเกี่ยวกับระบบปฏิบัติการที่ควรเขียน CPU แก้ไขตอนนี้ และใช่ฉันรู้ว่า AtomicReference เป็นเพียงกระดาษห่อหุ้มสำหรับเขตข้อมูลที่ผันผวน แต่ยังเพิ่มเป็นเอกสารประเภทหนึ่งที่เขตข้อมูลจะสามารถเข้าถึงได้โดยหลายเธรด
John Vint

@ จอห์นทำไมคุณถึงเพิ่มทิศทางอื่นผ่าน AtomicReference? หากคุณต้องการ CAS - ok แต่ AtomicUpdater อาจเป็นตัวเลือกที่ดีกว่า เท่าที่ฉันจำได้ไม่มีเนื้อแท้เกี่ยวกับ AtomicReference
bestsss

@bestsss สำหรับ purpouses ทั่วไปคุณมีสิทธิ์ไม่มีความแตกต่างระหว่าง AtomicReference.set / get และ volatile load and stores ที่ถูกกล่าวว่าฉันมีความรู้สึกเดียวกัน (และทำในระดับหนึ่ง) เกี่ยวกับเวลาที่จะใช้ซึ่ง รายละเอียดสามารถตอบสนองนี้มันบิตstackoverflow.com/questions/3964317/... การใช้อย่างใดอย่างหนึ่งเป็นสิ่งที่ต้องการมากกว่าอาร์กิวเมนต์เดียวของฉันสำหรับการใช้ AtomicReference เกี่ยวกับการระเหยง่าย ๆ นั้นมีไว้สำหรับเอกสารที่ชัดเจนซึ่งตัวมันเองไม่ได้สร้างข้อโต้แย้งที่ยิ่งใหญ่ที่สุดอย่างที่ฉันเข้าใจ
John Vint

ข้อสังเกตบางคนโต้แย้งโดยใช้ฟิลด์ที่ผันผวน / AtomicReference (โดยไม่ต้องใช้ CAS) นำไปสู่รหัส buggy old.nabble.com/…
John Vint

12

ในคำพูดของ Java Memory Model (ตามที่กำหนดไว้สำหรับ Java 5+ ใน JSR 133) การดำเนินการใด ๆ - อ่านหรือเขียนบนvolatileตัวแปรจะสร้างความสัมพันธ์ที่เกิดขึ้นก่อนหน้าซึ่งเกี่ยวข้องกับการดำเนินการอื่นใดในตัวแปรเดียวกัน ซึ่งหมายความว่าคอมไพลเลอร์และ JIT ถูกบังคับให้หลีกเลี่ยงการเพิ่มประสิทธิภาพบางอย่างเช่นการจัดลำดับคำสั่งใหม่ภายในเธรดหรือดำเนินการภายในแคชภายในเครื่องเท่านั้น

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

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


4

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

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


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

12
-1 การเข้าถึงตัวแปรระเหยค่อนข้างแตกต่างจากการใช้บล็อกที่ซิงโครไนซ์ การเข้าสู่บล็อกที่ซิงโครไนซ์ต้องใช้การเขียนแบบ atomic CompareAndSet เพื่อนำการล็อกออกและการเขียนแบบระเหยเพื่อปลดล็อก หากการล็อกเป็นที่พึงพอใจการควบคุมจะต้องผ่านจากพื้นที่ผู้ใช้ไปยังพื้นที่เคอร์เนลเพื่อตัดสินการล็อก (นี่คือบิตที่มีราคาแพง) การเข้าถึงสารระเหยจะยังคงอยู่ในพื้นที่ของผู้ใช้เสมอ
Michael Barker

@ MichaelBarker: คุณแน่ใจหรือว่าเคอร์เนลทุกจอต้องได้รับการปกป้องไม่ใช่แอป
Daniel

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

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