Matlab中變量拷貝的原理?
-- copy-on-write和mex參數傳遞機制解析
題記剖析:
C、C++語言里調用函數時有三種不同的傳參方式,分別為:傳值,傳址(即指針),傳引用。他們之間的 區別可以用下面的三句話高度概括:
傳值不改初衷, 傳址遠程操控, 引用就是別名。
當采用傳值的方式時,函數內的任何操作均不會對實參造成任何影響,而后面的兩種參數傳遞方式則可以對原始實參數據造成影響。為減少對原始數據的修改,Matlab統一采用了傳值的參數傳遞方式,函數內的任何讀寫操作均不會影響實參的原始數據。總而言之,在matlab里不要指望通過參數傳遞實現對原始數據的修改。如果需要修改,則需要借助matlab函數的返回結果進行間接操作。
本質概括:寫入時復制(英語:Copy-on-write,簡稱COW)是一種計算機程序設計領域的優化策略。字面意思:“”當write的時候才會copy"。 其核心思想是,如果有多個調用者(callers)同時要求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統才會真正復制一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。這過程對其他的調用者都是透明的(transparently)。此作法主要的優點是如果調用者沒有修改該資源,就不會有副本(private copy)被創建,因此多個調用者只是讀取操作時可以共享同一份資源。
Matlab真是讓人又愛又恨,諸多強大的工具包以及矩陣(向量)運算的便捷與高效讓人欲罷不能(那種一行代碼抵C++等效的10+行循環代碼的感覺真是爽到不行)。不過最近做實驗用Matlab讓我吃了不少苦頭,主要就是因為對它的copy-on-write機制以及mex文件的參數傳遞機制不了解。另外感覺這方面內容雖然一般不怎么涉及,但是對於理解Matlab並且充分發揮出它對矩陣運算的高效性而又不陷入無謂且難以察覺的臭蟲堆(切身體會啊!)十分重要,又加之中文的關於這方面的材料十分稀缺(我幾乎就沒見到……),所以在這里稍作總結,便於今后自己查閱也方便有需要的人。
1. 先解釋下2個關鍵詞:
a. copy-on-write:我們曉得變量在內存里存儲都有一個地址,而有的時候我們需要把一個變量A的值拷貝一份產生一個新的變量B。所謂copy-on-write說的就是制造這么一個假象,讓你覺得好像已經拷貝過了,但是其實只是讓那個新變量也指向原來的內存地址(因為這樣對於僅僅讀取變量值而言是等效的),只有當那個值必須發生改變的時候(或者說被寫入的時候)才趕緊把原先的內容拷貝出來,存到一個新的地方(如果改寫的是A,就存給B;如果改寫的是B,就存給A)。
說白了就是偷懶+說謊——直到要被戳穿了不得已為止。不過這樣做當然是有好處的,因為拷貝內存是有開銷的(即使是線性開銷),因此能省則省唄。特別對於Matlab這樣的動不動開個龐大矩陣做運算的語言來講,copy-on-write對於性能提升至關重要。
b. mex傳參:mex是"Matlab Executable"的縮寫,是個非常強大的機制。有了它以后,Matlab難以體現其優勢的代碼(比如多重循環)就可以轉而用C(C++貌似也可以)或者FORTRAN來實現,通過預先編譯產生動態鏈接庫文件(就是dll)為Matlab進程調用,並且調用接口與直接調用Matlab函數幾乎完全一樣(好吧,只是"幾乎"而已,不然我就不會碰到那些個bug了……)。
具體的mex介紹不是本文重點,就此略過。這里所謂的"mex傳參"就是指在Matlab里調用mex函數的時候,不同的傳參方式對於mex函數實際運行的影響。
2. Matlab的變量類型有蠻多種,為了后面方便討論,我們這里采用以下划分標准:
a. Scalar類型:就是單個的數值(實數或者虛數都可以,總之是個復數吧),也可以理解成1*1的矩陣;
b. Array類型:包括最簡單的向量(1*n或者n*1),矩陣(m*n)以及多維向量(n1*n2*...*nk),其中的元素是Scalar類型;
c. Structure類型:由一些Field組成(A.f1, A.f2, ...),其中每個Field值可以是Scalar類型、Array類型或者Structure類型;
d. 其他類型:所有不符合上述定義的其他合法類型(比如String類型,Cell類型等等)。
備注:我們這樣分類僅僅為了后面討論方便,因此Structure和Array都是狹義的,后文所得結論皆針對此狹義定義而言(倒也不是說對廣義的就不成立,是我懶得做進一步驗證了……)
3. 有兩種不同的情形都可能導致copy-on-write,即賦值與函數傳參,這里我們再做點小約束(假設A和B是兩個Matlab變量):
a. 賦值:就是單純的A=B,先不考慮對A或者B做任何附加操作,比如下標索引:A(1:5)=B(1:5)、算術運算:A=B.^2之類的;
b. 傳參:就是單純的f(A)(當然f(A, B, ...)對於我們討論A也無妨啦)
備注:其實對於很多附加操作,結論很容易推出來,不過,呃,為了嚴謹記么,就先不考慮它們了。
OK了,上結論!!!
4. copy-on-write:
a. 對於賦值情形:Scalar變量沒有copy-on-write;Array變量有;Structure變量的所有Field都是copy-on-write(不管該Field是不是Scalar類型變量),但是各管各的,互不影響。
舉例:A = 1; B = A; (B和A不共享數據,分別存在各自的內存地址里)
A = [1 2]; B = A; (B和A共享數據,沒有發生實際拷貝)
B(1) = 0; (copy-on-write!A的內存布局不變,B的數據存到新的地方)
A.f1 = 1; A.f2 = [1 1]; B = A; (B和A完全共享數據,包括A.f1和B.f1)
B.f1 = 2; (copy-on-write!A的內存布局不變,B.f2也不變,B.f1的數據存到新的地方)
備注:
1. 對於多維向量,因為實際存儲在內存中的時候和列向量並沒有區別,都是連續存儲的,所以發生copy-on-write的時候是整體拷貝,並不會因為只修改了其中某一維而繼續共享其他維的數據。
2. 這里(點我)舉報了Matlab的相關臭蟲一枚,感興趣的可以看下。大意是說,在函數里初始化一個向量,然后修改其中一個元素值,你說會不會copy-on-write咧?(答案是不一定!如果是這么初始化的:data=[0 0 0 0],那么修改data(1)=5之后data的數據整個copy了一份挪了窩;而如果是這樣呢:data(1:4)=0或者data=zeros(1,4)?那么修改data(1)=5之后數據還在原地,沒有拷貝!)
b. 對於傳參情形:比較清楚,統統都是copy-on-write!
備注:
1. 網上看到有的材料說Matlab可能在調用函數前進行pre-parse,先判斷一下參數在函數中會不會被修改,如果是read-only的函數,就copy-on-write;如果有修改的可能,就直接拷貝一份傳進去。但是我實驗下來並不是這樣!傳參的時候全部都是copy-on-write,也就是說一旦參數在函數里被修改,那么馬上拷貝一份新的出來,函數外的變量不會受到影響(具體的實驗方法后面會提到,也許對於不同版本的Matlab結論不同,列位看官也可以自己試一下)。
2. 這種機制並不完全盡如人意,比如這里(點我)就給Matlab的開發人員提了一個很好的問題,比如我現在有這么一個函數:
function data = transform(data)
data = data + 1;
end
其實我只需要這個函數幫我把data加1就完了,但是實際調用"A = transform(A)"的結果是:會發生2次A的拷貝!第1次是在data+1完了之后賦值更新data的時候(因為copy-on-write,傳參的時候其實並沒有拷貝),第2次是函數結束的時候把數據返回給A。copy-on-write把拷貝盡可能延遲當然很好,可是在這個情形下,我們其實更加希望——索性不要拷貝!呃,這個說起來其實也不是copy-on-write的錯啦……
5. mex傳參:這里先稍微介紹一下mex傳參的機制。其實Matlab里的所有Scalar變量和Array變量在內存里的存儲方式都差不多,就是從某一個首地址P開始連續存儲(對於矩陣或是多維數組而言,就是以前面的維數優先展開成向量存儲,比如一個2*2的矩陣A存起來就是A(1,1)、A(2,1)、A(1,2)、A(2,2)依次排開)。所以到了mex函數里,其實就只看到數組形式:想索引A(1,2)?就用a(2)吧(假設a是A對應的數組首地址)。再加上Matlab函數傳參的copy-on-write機制(注意傳參發生時,還在Matlab中),我們其實在mex函數里就可以直接取到參數的實際內存地址(還沒有發生任何拷貝哦~),就像是在傳引用。
因此針對上面的備注2里提到的問題,我們就可以利用mex巧妙地加以解決。怎么做呢?寫一個mex文件,沒有返回值(調用的時候直接寫transform(A)),實現"data = data + 1"的等效功能,然后利用copy-on-write!它保證把參數數據的真實內存地址信息遞交給mex函數(用mxGetPr可以取到首地址,用mxGetDimensions可以取到維數信息,其實也就是數據長度),之后就可以直接對數據進行操作啦,完事之后直接返回,一次拷貝都沒有!不必擔心copy-on-write在你改寫數據的時候出來搗亂,因為你的改寫發生在mex函數里,動態加載運行以后Matlab移交控制權,對其無能為力,想要拷貝也無法了!
很贊是不是?知道了是很贊,可是假設你不曉得這其中有這些小秘密咧(比如昨天的我)……就會莫名奇妙地發現調用函數以后——誰動了我的參數!以后可得注意了。
硬幣的另一面是:有時候我們真的需要在函數里修改參數值,但是又不希望函數外的變量受到影響,怎么辦?(如果不使用mex,這個問題根本不存在,因為Matlab的copy-on-write保證了這一點)
這里提供一個辦法,或曰小伎倆,就是在傳參的時候不要老老實實傳,像這樣:f(A),而是加一個全程下標索引,像這樣:f(A(1:end))!
為什么這樣可行呢?
因為當Matlab知道你要傳的參數僅僅是真實數據的其中一部分的時候(我們只不過用"1:end"就能輕易地騙倒它),原先的"連續存儲"就失效了(多維向量的連續下標索引出來的數據在一維展開的情形下一般都是散落在內存各處的),但是mex函數接受的參數必須是數組形式——換言之,在內存里是連續存儲的,所以Matlab只好把這些數據拷貝出來重新在內存里連續排好,再傳給mex函數——也就是說,我們強制Matlab在mex傳參的時候發生一次參數拷貝。然后我們就能在mex函數里放心地對參數為所欲為了~
不過這樣還帶來了一個副產品——有時候我們希望在mex傳參的時候把實際是整型的變量用int32轉換之后再傳(Matlab默認的類型都是double)。在這個轉換的過程中,因為double和int類型在內存中的表示是截然不同的,因此必然導致原來的參數數據不能直接傳給mex,所以這種時候也會發生"拷貝"。
總結一下就是:Matlab里的mex傳參在直接引用參數的情況下(比如f(A))就相當於傳引用,mex函數直接對參數進行讀寫操作,不發生參數拷貝;而在原參數的內存存儲形式不能直接以數組形式傳遞給mex函數的時候,就會發生參數拷貝,從而mex函數對參數所作的任何修改都對外透明。
6. 文中關於copy-on-write的結論部分來自於這里與這里,且都經過本人實驗證實,實驗方法為采用"format debug"命令(很遺憾,似乎該命令沒有文檔說明……)。基本用法如下:
>> format debug
>> a = magic(2) % 注意不要打分號";"
a =
Structure address = 16849840 % a變量的結構體地址(元信息)
m = 2 % 行數
n = 2 % 列數
pr = 1a321130 % 實部內存首地址
pi = 0 % 虛部內存首地址
1 3 % 數據內容
4 2
通過設斷點和觀察輸出就可以看到各變量的內存地址了,進而可以甄別到底是發生instant-copy還是copy-on-write。
關於"Structure address"(以下簡稱"Sa")到底是個什么東西,我曾經以為我知道,但是做了幾個實驗以后就徹底迷惑了。實驗結果是——對Scalar或者Array變量進行賦值,得到新的Sa;對Structure類型變量進行賦值,Sa不變;傳參時不管是Scalar/Array/Structure變量,都得到新的Sa……哪位高人給指點指點!
文中關於mex傳參的結論來源於我的切身的慘痛的經歷,沒有進行進一步查證,不過我覺得我的解釋挺靠譜的,列為看官愛信不信~