日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

指令引用了 内存 该内存不能为read 一直弹窗_【翻译】使用Rust测试ARM和X86内存模型

發(fā)布時(shí)間:2023/12/1 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 指令引用了 内存 该内存不能为read 一直弹窗_【翻译】使用Rust测试ARM和X86内存模型 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

原文標(biāo)題: The Story of Tail Call Optimizations in Rust 原文標(biāo)題: Examining ARM vs X86 Memory Models with Rust
原文鏈接: https://www.nickwilcox.com/blog/arm_vs_x86_memory_model/
公眾號: Rust碎碎念

蘋果公司最近宣布,他們將要把筆記本和桌面電腦從Intel x86 CPU 遷移到自研的ARM架構(gòu)的CPU。我認(rèn)為是時(shí)候來看一下這兩者之間那些會(huì)對使用Rust工作的系統(tǒng)程序員有影響的區(qū)別了。
ARM架構(gòu)的CPU不同于X86 CPU的很重要的一點(diǎn)是它們的內(nèi)存模型。這篇文章將會(huì)討論什么是內(nèi)存模型以及它是如何讓代碼在一種CPU架構(gòu)上正確運(yùn)行而在另一種CPU架構(gòu)上引起競爭條件(race condition)。

內(nèi)存模型

特定CPU上多個(gè)線程之間交互時(shí)對內(nèi)存進(jìn)行加載(load)和存儲(store)的方式稱為該架構(gòu)的內(nèi)存模型。
根據(jù)CPU的內(nèi)存模型的不同,一個(gè)線程的多次寫入操作可能會(huì)被另一個(gè)線程以不同的順序可見。
進(jìn)行多次讀取操作的線程也是如此。一個(gè)正在進(jìn)行多次讀取操作的線程可能收到全局狀態(tài)的“快照”,這些狀態(tài)表示的時(shí)間順序不同于事實(shí)上發(fā)生的順序。
現(xiàn)代硬件需要這種靈活性從而能夠最大化內(nèi)存操作的吞吐量。每次CPU的更新?lián)Q代就會(huì)提升CPU的時(shí)鐘頻率和核數(shù),但是內(nèi)存帶寬一直在努力追趕保持同步。將數(shù)據(jù)從內(nèi)存中取出進(jìn)行操作通常是應(yīng)用程序的性能瓶頸。
如果你從來沒有寫過多線程代碼,或者僅僅使用高級同步原語,如std::sync::Mutex來完成任務(wù),那你可能從來沒有接觸過內(nèi)存模型的細(xì)節(jié)。這是因?yàn)?#xff0c;不管CPU的內(nèi)存模型允許它執(zhí)行什么樣的重新排序,它總是對當(dāng)前線程呈現(xiàn)出一致的內(nèi)存視圖。
如果我們看一下下面的代碼片段,這段代碼寫入內(nèi)存然后直接讀取相同的內(nèi)存,當(dāng)我們進(jìn)行讀取時(shí),我們總能按照預(yù)期讀到58。我們永遠(yuǎn)不會(huì)從內(nèi)存中讀取過時(shí)的值。

pub unsafe fn read_after_write(u32_ptr: *mut u32) {u32_ptr.write_volatile(58);let u32_value = u32_ptr.read_volatile();println!("the value is {}", u32_value); }

我之所以使用volatile操作是因?yàn)槿绻沂褂闷胀ǖ闹羔槻僮?#xff0c;編譯器就會(huì)足夠聰明地跳過內(nèi)存讀取而直接打印出58。Volatile操作阻止編譯器重排序或跳過內(nèi)存操作。但是,他們對硬件沒有影響(或者說,編譯器重排序相對于非易失性內(nèi)存操作)。
一旦我們引入了多線程,我們就會(huì)面臨這樣一個(gè)事實(shí):CPU可能對我們的內(nèi)存操作重排序。
我們可以在多線程環(huán)境中測試下面的代碼片段:

pub unsafe fn writer(u32_ptr_1: *mut u32, u32_ptr_2: *mut u32) {u32_ptr_1.write_volatile(58);u32_ptr_2.write_volatile(42); }pub unsafe fn reader(u32_ptr_1: *mut u32, u32_ptr_2: *mut u32) -> (u32, u32) {(u32_ptr_1.read_volatile(), u32_ptr_2.read_volatile()) }

如果我們把兩個(gè)指針指向的內(nèi)容都初始化為0, 然后每個(gè)函數(shù)放在不同的線程中運(yùn)行,我們可以列出可能讀取到的結(jié)果。我們知道,雖然沒有同步機(jī)制,但是基于我們對單線程中代碼的經(jīng)驗(yàn),我們可以想到可能的返回值是(0,0),(58,0),(58,42)。但是硬件對內(nèi)存寫操作的重排序可能會(huì)影響多線程,這意味著,還有第四種可能性(0,42)。
你可能認(rèn)為,由于缺少同步機(jī)制,可能會(huì)產(chǎn)生更多的可能性。但是所有的硬件內(nèi)存模型保證了原生字(word)對齊的加載(load)和存儲(store)是原子性的(32位CPU的u32類型,64位CPU的u64類型)。如果我們把其中一個(gè)寫入改為0xFFFF_FFFF,讀取操作將永遠(yuǎn)只能看到舊值或新值。它將不會(huì)看到一個(gè)不完整的值,比如0xFFFF_0000。
當(dāng)使用常規(guī)方式訪問內(nèi)存時(shí),如果CPU的內(nèi)存模型的細(xì)節(jié)被隱藏起來,當(dāng)其影響到程序的正確性時(shí),似乎我們就沒有辦法在多線程程序中對其進(jìn)行控制。
幸運(yùn)地是,Rust提供了如std::sync::atomic這樣的模塊,其中提供了能夠滿足我們控制需要的類型。我們使用這些類型來明確指定我們的代碼所需要的內(nèi)存序(memory order)要求。我們用性能換取正確性。我們對硬件執(zhí)行內(nèi)存操作的順序進(jìn)行了限制,取消了硬件希望執(zhí)行的帶寬優(yōu)化。
當(dāng)使用atomic模塊進(jìn)行工作的時(shí)候,我們不用擔(dān)心各個(gè)CPU架構(gòu)上的實(shí)際的內(nèi)存模型。atomic模塊工作在一個(gè)抽象的內(nèi)存模型之上,對底層CPU并不知道。一旦我們在使用Rust內(nèi)存模型時(shí)表明我們對加載(load)和存儲(store)的需求,編譯器就會(huì)將其映射到目標(biāo)CPU的內(nèi)存模型上。
我們對于每個(gè)操作的要求表現(xiàn)為我們想要在操作上允許(或拒絕)什么樣的重排序。次序形成了一個(gè)層級,每一層對CPU進(jìn)行了更多的限制。例如,Ordering::Relaxed意味著CPU可以自由執(zhí)行任意的重排序。Ordering::Release意味著一個(gè)存儲(store)操作只能在所有正在進(jìn)行的存儲完成結(jié)束之后才能完成。
讓我們來看看,原子內(nèi)存寫操作相比較于常規(guī)寫操作,實(shí)際上是怎么編譯的。

use std::sync::atomic::*;pub unsafe fn test_write(shared_ptr: *mut u32) {*shared_ptr = 58; }pub unsafe fn test_atomic_relaxed(shared_ptr: &AtomicU32) {shared_ptr.store(58, Ordering::Relaxed); }pub unsafe fn test_atomic_release(shared_ptr: &AtomicU32) {shared_ptr.store(58, Ordering::Release); }pub unsafe fn test_atomic_consistent(shared_ptr: &AtomicU32) {shared_ptr.store(58, Ordering::SeqCst); }

如果我們看一下上面的代碼生成的 X86 匯編,我們會(huì)看到前三個(gè)函數(shù)產(chǎn)生了相同的代碼。直到更加嚴(yán)格的SeqCst次序,我們才得到一個(gè)生成的不同的指令集。

example::test_write:mov dword ptr [rdi], 58retexample::test_atomic_relaxed:mov dword ptr [rdi], 58retexample::test_atomic_release:mov dword ptr [rdi], 58retexample::test_atomic_consistent:mov eax, 58xchg dword ptr [rdi], eaxret

前面兩個(gè)次序,使用MOV(MOVe)指令把值寫到內(nèi)存。只有更嚴(yán)格的次序生成了不同的指令,XCHG(atomic eXCHanG),來對一個(gè)原生指針進(jìn)行寫操作。
我們可以和生成的ARM匯編進(jìn)行比較:

example::test_write:mov w8, #58str w8, [x0]retexample::test_atomic_relaxed:mov w8, #58str w8, [x0]retexample::test_atomic_release:mov w8, #58stlr w8, [x0]retexample::test_atomic_consistent:mov w8, #58stlr w8, [x0]ret

和之前相反,在我們達(dá)到release次序要求之后可以看到一些不同。原生指針和relax原子存儲操作使用STR(STore Register)而release和sequential次序使用指令STLR(STore with reLease Register)。在這段匯編代碼里,MOV指令把常量58移動(dòng)到一個(gè)寄存器,它不是一個(gè)內(nèi)存操作。
我們應(yīng)該能夠看出這里的風(fēng)險(xiǎn),即對程序員的錯(cuò)誤更加寬容。對我們而言,在抽象內(nèi)存模型上寫出錯(cuò)誤的代碼但是讓它在某些CPU上產(chǎn)生正確的匯編代碼并且正確工作也是有可能的。

使用Atomic寫一個(gè)多線程程序

我們將要討論的程序是構(gòu)建于存儲一個(gè)指針值是跨線程原子操作這一概念之上的。一個(gè)線程將要使用自己擁有的一個(gè)可變對象來執(zhí)行某項(xiàng)任務(wù)。一旦它結(jié)束了那項(xiàng)任務(wù),它將會(huì)以一個(gè)不可變的共享引用來發(fā)布該任務(wù),使用一個(gè)原子指針寫入工作完成的信號并且允許讀線程使用數(shù)據(jù)。

僅X86模式下的實(shí)現(xiàn)

如果我們真的想要測試X86的內(nèi)存模型有多么寬容(forgiving 譯者注:這里暫未想到更合適的翻譯 ),我們可以寫一段跳過任意使用了std::sync::atomic模塊的代碼。我想強(qiáng)調(diào)的是,這不是你真正應(yīng)該考慮做的事情。事實(shí)上,由于沒有保證避免編譯器對指令的重排序,所以這段代碼有未定義行為(盡管如此,Rust1.44.1版編譯器沒有進(jìn)行"重排序",所以這段代碼可以"工作")。這僅僅是個(gè)用作學(xué)習(xí)的小練習(xí)。

pub struct SynchronisedSum {shared: UnsafeCell<*const u32>,samples: usize, }impl SynchronisedSum {pub fn new(samples: usize) -> Self {assert!(samples < (u32::MAX as usize));Self {shared: UnsafeCell::new(std::ptr::null()),samples,}}pub fn generate(&self) {// do work on data this thread ownslet data: Box<[u32]> = (0..self.samples as u32).collect();// publish to other threadslet shared_ptr = self.shared.get();unsafe {shared_ptr.write_volatile(data.as_ptr());}std::mem::forget(data);}pub fn calculate(&self, expected_sum: u32) {loop { // check if the work has been published yetlet shared_ptr = self.shared.get();let data_ptr = unsafe { shared_ptr.read_volatile() };if !data_ptr.is_null() {// the data is now accessible by multiple threads, treat it as an immutable reference.let data = unsafe { std::slice::from_raw_parts(data_ptr, self.samples) };let mut sum = 0;for i in (0..self.samples).rev() {sum += data[i];}// did we access the data we expected?assert_eq!(sum, expected_sum);break;}}} }

計(jì)算數(shù)組之和的函數(shù)從執(zhí)行一個(gè)循環(huán)開始,這個(gè)循環(huán)里會(huì)讀取共享指針的值。因?yàn)槲覀円阎脑哟鎯ΡWC所以read_volatile()只返回null或者一個(gè)指向u32slice的指針。我們不斷地進(jìn)行循環(huán)直到生成線程結(jié)束并且發(fā)布它的工作。一旦它被發(fā)布,我們就能讀取到它并且計(jì)算元素的和。

測試代碼

作為一個(gè)簡單的測試,我們將要同時(shí)運(yùn)行兩個(gè)線程,一個(gè)用來生成值另一個(gè)用來計(jì)算總和。兩個(gè)線程執(zhí)行完各自的工作之后都會(huì)退出,我們通過使用join來等待它們退出。

pub fn main() {print_arch();for i in 0..10_000 {let sum_generate = Arc::new(SynchronisedSum::new(512));let sum_calculate = Arc::clone(&sum_generate);let calculate_thread = thread::spawn(move || {sum_calculate.calculate(130816);});thread::sleep(std::time::Duration::from_millis(1));let generate_thread = thread::spawn(move || {sum_generate.generate();});calculate_thread.join().expect(&format!("iteration {} failed", i));generate_thread.join().unwrap();}println!("all iterations passed"); }

如果我在一個(gè)Intel的CPU上運(yùn)行測試,我會(huì)得到下面的結(jié)果:

running on x86_64 all iterations passed

如果我在一個(gè)具有兩個(gè)核的ARM CPU上運(yùn)行測試,我會(huì)得到:

running on aarch64 thread '<unnamed>' panicked at 'assertion failed: `(left == right)`left: `122824`,right: `130816`', srcmain.rs:45:17 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'main' panicked at 'iteration 35 failed: Any', srcmain.rs:128:9

X86處理器能夠成功運(yùn)行10000次測試,但是ARM處理器在第35次運(yùn)行失敗了。

哪里出問題了?

在我們執(zhí)行最后的寫入共享指針將其發(fā)布給其他線程之前,我們模式的正常運(yùn)作要求我們正在進(jìn)行的“工作(work)”在內(nèi)存中處于正確的狀態(tài)。
ARM的內(nèi)存模型不同于X86內(nèi)存模型的地方在于ARM CPU將會(huì)對寫入操作進(jìn)行重排序,而X86不會(huì)。所以,計(jì)算線程能夠看到一個(gè)非空(non-null)的指針并且在slice還沒被寫入之前就開始從其中讀取值。
對于我們程序中的大多數(shù)內(nèi)存操作,我們想要給CPU足夠的自由來重新整理操作從而使性能最大化。我們只想要指定最小的必要性約束來確保正確性。
至于我們的generate函數(shù), 我們想要slice中的值以任意能夠帶來最快速度的順序?qū)懭雰?nèi)存。但是,所有的寫入必須在我們把值寫入共享指針之前完成。
在calculate函數(shù)上正好相反。我們有一個(gè)要求,從slice內(nèi)存中讀取的值至少和共享指針中的值來自相同的時(shí)間點(diǎn)。
盡管在對共享指針的讀取完成之前不會(huì)執(zhí)行這些指令,但我們需要確保不會(huì)從過期的緩存中得到這些值。

正確的版本

為了確保我們代碼的正確性,對共享指針的寫入必須使用release次序,并且由于calculate的讀取順序要求,我們使用acquire次序。
我們對數(shù)據(jù)的初始化以及計(jì)算總和的代碼都沒有改變,我們想給CPU足夠的自由以最高效的方式來運(yùn)行。

struct SynchronisedSumFixed {shared: AtomicPtr<u32>,samples: usize, }impl SynchronisedSumFixed {fn new(samples: usize) -> Self {assert!(samples < (u32::MAX as usize));Self {shared: AtomicPtr::new(std::ptr::null_mut()),samples,}}fn generate(&self) {// do work on data this thread ownslet mut data: Box<[u32]> = (0..self.samples as u32).collect();// publish (aka release) this data to other threadsself.shared.store(data.as_mut_ptr(), Ordering::Release);std::mem::forget(data);}fn calculate(&self, expected_sum: u32) {loop {let data_ptr = self.shared.load(Ordering::Acquire);// when the pointer is non null we have safely acquired a reference to the global dataif !data_ptr.is_null() {let data = unsafe { std::slice::from_raw_parts(data_ptr, self.samples) };let mut sum = 0;for i in (0..self.samples).rev() {sum += data[i];}assert_eq!(sum, expected_sum);break;}}} }

如果我們在ARM CPU上運(yùn)行使用了AtomicPtr<u32>更新后的版本,我們會(huì)得到:

running on aarch64 all iterations passed

次序的選擇

在跨多個(gè)CPU進(jìn)行工作的時(shí)候,使用atomic模塊仍然需要注意。正如我們看到的X86和ARM匯編代碼的輸出,如果我們在store上使用Ordering::Relaxed來替換Ordering::Release,我們能回退到一個(gè)在x86上正確運(yùn)行但是在ARM上會(huì)失敗的版本。使用AtomicPtr尤其需要在最終訪問指針指向的值的時(shí)候避免未定義行為。

延伸閱讀

這只是對內(nèi)存模型的一個(gè)簡要介紹,希望對這個(gè)主題不熟悉的小伙伴們能有個(gè)清晰的認(rèn)知。 - ARM V-8內(nèi)存模型細(xì)節(jié) - Intel X86 內(nèi)存模型細(xì)節(jié) - Rust的atomic模塊內(nèi)存序引用

我的第一篇介紹無鎖編程的文章是這篇。這篇文章看起來可能和內(nèi)存模型不太相關(guān),因?yàn)樗顷P(guān)于C++, Xbox360上的PowerPC CPU以及Windows API的一些細(xì)節(jié)。但是,它仍然是對這些原則的一個(gè)很好的解釋。而且下面這段話從開始到現(xiàn)在都站得住腳:

無鎖編程一種有效的多線程編程技術(shù),但是不應(yīng)該輕易使用。在使用它之前,你必須理解它的復(fù)雜性,并且你應(yīng)該仔細(xì)評估以確保它真正能帶來預(yù)期的益處。在很多情況下,應(yīng)該使用更簡潔高效的解決方案,比如更少地使用共享數(shù)據(jù)。

總結(jié)

希望我們已經(jīng)了解了關(guān)于系統(tǒng)編程的一個(gè)新的方面,隨著ARM芯片的越來越普及,這方面的知識會(huì)更加重要。確保原子性的代碼從來都不簡單,而當(dāng)其跨不同架構(gòu)下的不同內(nèi)存模型時(shí),就變得更加困難了。

總結(jié)

以上是生活随笔為你收集整理的指令引用了 内存 该内存不能为read 一直弹窗_【翻译】使用Rust测试ARM和X86内存模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。