指令引用了 內存 該內存不能為read 一直彈窗_【翻譯】使用Rust測試ARM和X86內存模型

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

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

內存模型

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

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操作是因為如果我使用普通的指針操作,編譯器就會足夠聰明地跳過內存讀取而直接打印出58Volatile操作阻止編譯器重排序或跳過內存操作。但是,他們對硬件沒有影響(或者說,編譯器重排序相對于非易失性內存操作)。
一旦我們引入了多線程,我們就會面臨這樣一個事實:CPU可能對我們的內存操作重排序。
我們可以在多線程環境中測試下面的代碼片段:

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())
}

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

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 匯編,我們會看到前三個函數產生了相同的代碼。直到更加嚴格的SeqCst次序,我們才得到一個生成的不同的指令集。

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

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

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

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

使用Atomic寫一個多線程程序

我們將要討論的程序是構建于存儲一個指針值是跨線程原子操作這一概念之上的。一個線程將要使用自己擁有的一個可變對象來執行某項任務。一旦它結束了那項任務,它將會以一個不可變的共享引用來發布該任務,使用一個原子指針寫入工作完成的信號并且允許讀線程使用數據。

僅X86模式下的實現

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

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;}}}
}

計算數組之和的函數從執行一個循環開始,這個循環里會讀取共享指針的值。因為我們已知的原子存儲保證所以read_volatile()只返回null或者一個指向u32slice的指針。我們不斷地進行循環直到生成線程結束并且發布它的工作。一旦它被發布,我們就能讀取到它并且計算元素的和。

測試代碼

作為一個簡單的測試,我們將要同時運行兩個線程,一個用來生成值另一個用來計算總和。兩個線程執行完各自的工作之后都會退出,我們通過使用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");
}

如果我在一個Intel的CPU上運行測試,我會得到下面的結果:

running on x86_64
all iterations passed

如果我在一個具有兩個核的ARM CPU上運行測試,我會得到:

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處理器能夠成功運行10000次測試,但是ARM處理器在第35次運行失敗了。

哪里出問題了?

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

正確的版本

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

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上運行使用了AtomicPtr<u32>更新后的版本,我們會得到:

running on aarch64
all iterations passed

次序的選擇

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

延伸閱讀

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

我的第一篇介紹無鎖編程的文章是這篇。這篇文章看起來可能和內存模型不太相關,因為它是關于C++, Xbox360上的PowerPC CPU以及Windows API的一些細節。但是,它仍然是對這些原則的一個很好的解釋。而且下面這段話從開始到現在都站得住腳:

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

總結

希望我們已經了解了關于系統編程的一個新的方面,隨著ARM芯片的越來越普及,這方面的知識會更加重要。確保原子性的代碼從來都不簡單,而當其跨不同架構下的不同內存模型時,就變得更加困難了。

0e9366ac65f5e86050521a4be745920e.gif

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/370533.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/370533.shtml
英文地址,請注明出處:http://en.pswp.cn/news/370533.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Docker應用二:docker常用命令介紹

Docker常用命令使用介紹 docker中常用的命令: 1、docker search image_name:搜查鏡像 2、docker pull image_name:從鏡像庫中拉去鏡像 3、docker run image_name:運行容器 --restartalways:容器退出后重新啟動 --name:自定容器名字 --d:后臺運行容器 --i:交互模式 --t:打開一個…

關于Ubuntu使用筆記

Ubuntu vm tools 安裝 sudo apt install open-vm-tools-desktop 在安裝程序時Ubuntu會將安裝目錄鎖定&#xff0c;安裝結束后會解除鎖定&#xff0c;中斷安裝后無法再安裝其他軟件解決方案 E: Could not get lock /var/lib/dpkg/lock - open (11: Resource temporarily unavail…

具有可執行Tomcat的獨立Web應用程序

在部署應用程序時&#xff0c;簡單性是最大的優勢。 您將了解到&#xff0c;尤其是在項目發展且需要在環境中進行某些更改時。 將整個應用程序打包到一個獨立且自足的JAR中似乎是個好主意&#xff0c;尤其是與在目標環境中安裝和升級Tomcat相比。 過去&#xff0c;我通常將Tomc…

css網頁中設置背景圖片的方法詳解

在css代碼中設置背景圖片的方法&#xff0c;包括背景圖片、背景重復、背景固定、背景定位等 用css設置網頁中的背景圖片&#xff0c;主要有如下幾個屬性&#xff1a; 1&#xff0c;背景顏色 {">說明&#xff1a;參數取值和顏色屬性一樣 注意&#xff1a;在HTML當中&am…

node-sass安裝不成功的問題

SASS_BINARY_SITEhttps://npm.taobao.org/mirrors/node-sass/ npm install node-sass 簡單粗暴的執行上述的命令。轉載于:https://www.cnblogs.com/czaiz/p/6918114.html

npm升級依賴包_Taro跨端開發之依賴管理

昨天跑的好好項目,今天跑不起來我們在開發周期比較長的前端項目的時候,必然會遇到依賴管理的問題. 我們在開發項目的時候,我們用了大量的三方庫.這些三方的依賴庫時不時的會更新自己的代碼.第三方依賴庫的代碼更新會很容易造成代碼運行的不穩定, 比如昨天還跑的好好的項目,另一…

QOTD:Java線程與Java堆空間

以下問題很常見&#xff0c;并且與OutOfMemoryError有關&#xff1a;在JVM線程創建過程和JVM線程容量期間無法創建新的本機線程問題。 這也是我向新技術候選人&#xff08;高級職位&#xff09;提出的典型面試問題。 我建議您在查看答案之前嘗試提供自己的答復。 題&#xff1…

sql查詢重復項

select * from [表A] where id in (select id from [表A] group by id having count(id) >1 )轉載于:https://www.cnblogs.com/wuyujie/p/7885017.html

java util logging_簡單日志記錄,使用java.util.logging

jspservletJavaBean模式下,可以做個簡單的日志記錄,日志文件保存在服務器.(Tomcat)package controller;import java.io.File;import java.io.IOException;import java.util.logging.FileHandler;import java.util.logging.Level;import java.util.logging.Logger;import javax.…

超級高鐵

超級高鐵 作者&#xff1a;武培&#xff0c;高培焱 作品來源&#xff1a;實踐 美國電動汽車公司特斯拉和美國科技公司ET3都公布了“真空管鋼運輸”計劃&#xff0c;特斯拉將其命名為“超級高鐵”&#xff0c;ET3因列車外觀酷似膠囊因而稱之為“吃膠囊”列車。根據ET3公司的介紹…

使用Spring @Autowired List的責任鏈

在Spring 3.1中&#xff0c;有一種方法可以自動填充類型化的List&#xff0c;這在您想在代碼中稍微進行去耦和清理時非常方便。 為了向您展示它是如何工作的&#xff0c;我將實現一個簡單的責任鏈&#xff0c;該責任鏈將為通過的用戶打印一些問候。 讓我們從我們擁有的&#…

設計模式 建造者模式 與 Spring Bean建造者 BeanDefinitionBuilder 源碼與應用

建造者模式 定義: 將一個復雜對象的構建與它的表示分離&#xff0c;使得同樣的構建過程可以創建不同的表示主要作用: 在用戶不知道對象的建造過程和細節的情況下就可以直接創建復雜的對象如何使用: 用戶只需要給出指定復雜對象的類型和內容, 建造者模式負責按順序創建復雜對象…

java 布隆過濾器_什么是布隆過濾器(Bloom Filter)?

在日常工作中&#xff0c;有一個比較常見的需求&#xff0c;就是需要判斷一個元素是否在集合中。例如以下場景&#xff1a;給定一個IP黑名單庫&#xff0c;檢查指定IP是否在黑名單中&#xff1f;在接收郵件的時候&#xff0c;判斷一個郵箱地址是否為垃圾郵件&#xff1f;在文字…

STM32上使用JSON

一、STM32工程中添加JSON 最近在一網2串項目&#xff0c;串口和網口之間可能需要定義一下簡單的通信協議&#xff0c;而通信協議上則需要去定義一下通信的數據格式&#xff0c;上次聽劍鋒說要用Json來定義&#xff0c;目前查了下資料具體如何去應用還不 會。因為最新的KEIL上支…

Flex 學習

Flex案例一&#xff1a; 1 <html>2 <head>3 <meta http-equiv"Content-Type" content"text/html; charsetutf-8" /> 4 <title>無標題</title>5 <style type"text/css">6 body,h1,h2,h3,h4,…

Cocos2d-X中實現自己定義菜單處理事件

當用戶點擊再松開后才會響應菜單事件&#xff0c;而在游戲中有些游戲須要玩家點擊后就處理事件。如玩坦克大戰的時候&#xff0c;玩家是點擊一下就發射子彈。并是點擊松手后發射子彈&#xff0c;在Cocos2d-X中沒有這樣的消息。以下就通過自己定義的方式實現當用戶點擊后就調用處…

java linkedhashset_java之LinkedHashSet

LinkedHashSet是Set集合的一個實現&#xff0c;具有set集合不重復的特點&#xff0c;同時具有可預測的迭代順序&#xff0c;也就是我們插入的順序。并且linkedHashSet是一個非線程安全的集合。如果有多個線程同時訪問當前linkedhashset集合容器&#xff0c;并且有一個線程對當前…

使用Spring Integration輪詢http端點

如果您想用Spring Integration編寫一個流程來輪詢HTTP端點并從http端點收集一些內容以進行進一步處理&#xff0c;那有點不直觀。 Spring Integration提供了幾種與HTTP端點集成的方式- Http出站適配器–將消息發送到http端點 Http出站網關–將消息發送到http端點并收集響應作…

python模塊離線安裝_離線安裝db2的python模塊ibm_db

1、為什么要離線安裝 沒網&#xff0c;在銀行工作&#xff0c;服務器環境配置&#xff0c;完全離線&#xff08;本來五分鐘搞定的事情&#xff0c;非要搞一天。我服&#xff01;&#xff01;&#xff09; 2、安裝步驟 視情況而定。 3。一個下載db2的client包&#xff0c;官網下…

Jmeter 場景設計

今天的業務場景是&#xff1a; 1.管理員登錄后臺---登錄成功后添加一個某類型的產品---產品添加成功后&#xff0c;再為該產品添加10個排期。 2.管理員登錄后臺--登錄成功后添加多個不同類型產品---產品全部添加完成后&#xff0c;依次為所有產品添加10個排期。 這是兩種不同的…