2016-07-30 9 views
12

C#/。NET通過傳遞Array類型的引用(與C/C++相比,它將所有值直接放在堆棧上,不管是好還是壞)都具有可變參數函數參數。C#中的`params`會不會在每次調用時分配一個新的數組?

在C#世界,這有讓您調用同一個函數以「生」的論點或可重複使用的陣列實例的一個整潔的優勢:

CultureInfo c = CultureInfo.InvariantCulture; 

String formatted0 = String.Format(c, "{0} {1} {2}", 1, 2, 3); 

Int32 third = 3; 
String formatted0 = String.Format(c, "{0} {1} {2}", 1, 2, third); 

Object[] values = new Object[] { 1, 2, 3 }; 
String formatted1 = String.Format(c, "{0} {1} {2}", values); 

這意味着產生CIL相當於:

String formatted0 = String.Format(c, "{0} {1} {2}", new Object[] { 1, 2, 3 }); 

Int32 third = 3; 
String formatted0 = String.Format(c, "{0} {1} {2}", new Object[] { 1, 2, third }); 

Object[] values = new Object[] { 1, 2, 3 }; 
String formatted1 = String.Format(c, "{0} {1} {2}", values); 

這意味着(在非優化的JIT編譯器)每次調用將分配一個新的Object[]實例 - 儘管在第三個例子你能存儲陣列作爲字段或其他可重複使用的值來消除上的新分配,每調用String.Format

但是在官方CLR運行時和JIT中是否進行了任何優化以消除此分配?或者可能數組是特意標記的,以便在執行離開呼叫站點的範圍後立即釋放數組?或者,也許是因爲C#或JIT編譯器知道參數的數量(使用「raw」時)是否可以執行與stackalloc關鍵字相同的操作,並將該數組放在堆棧上,因此不需要解除分配它?

回答

7

是的,每次都會分配一個新的數組。

不,沒有優化。沒有你提出的那種「實習」。畢竟,怎麼會有?接收方法可以對數組執行任何操作,包括對其成員進行變異,或者重新分配數組條目,或者將數組的引用傳遞給其他數組(然後不包括params)。

沒有特別的「標籤」,你建議的那種存在。這些數組以與其他任何方式相同的方式進行垃圾回收。


增加:當然有一個特殊的情況下,我們在這裏討論的那種「實習」,可以很容易的事,那就是爲長度爲零的數組。 C#編譯器可能會調用Array.Empty<T>()(每次返回相同的長度爲零的數組),而不是在需要調用params(其中需要長度爲零的數組)時創建new T[] { }

這種可能性的原因是長度爲零的數組是真正不可變的。

當然長度爲零的陣列的「實習」將是可發現的,例如這個類的行爲會改變如果特徵分別加以介紹:

class ParamsArrayKeeper 
{ 
    readonly HashSet<object[]> knownArrays = new HashSet<object[]>(); // reference-equals semantics 

    public void NewCall(params object[] arr) 
    { 
    var isNew = knownArrays.Add(arr); 
    Console.WriteLine("Was this params array seen before: " + !isNew); 
    Console.WriteLine("Number of instances now kept: " + knownArrays.Count); 
    } 
} 

增加:鑑於「奇怪的」陣列協方差。NET並不適用於價值類型,你確定你的代碼:

Int32[] values = new Int32[ 1, 2, 3 ]; 
String formatted1 = String.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", values); 

按預期工作(如果語法修正爲new[] { 1, 2, 3, }或類似的,這會去的String.Format錯誤的過載,肯定)。

+0

重新沒有實習生:大不了!如果一個方法在不知道它來自哪裏以及之後會發生什麼變化的情況下改變了一個數組,並且期望它可以正常工作,它就應該得到它。 – jmoreno

5

但是在官方的CLR運行時和JIT中是否做了任何優化來消除這種分配?

你必須問作者。但考慮到需要付出多少努力,我對此表示懷疑。聲明方法必須有權訪問數組,並使用數組語法檢索成員。所以任何優化都必然會導致不得不重寫方法邏輯來將數組訪問轉換爲直接參數訪問。

此外,優化將不得不全球發生,考慮到全部調用者的方法。並且它必須檢測該方法是否將該數組傳遞給其他任何內容。

這似乎並不像一個可行的優化,尤其是考慮到它將增加運行時性能的價值。

或者可能是專門標記的數組,以便在執行離開呼叫站點的範圍後立即解除分配數組?

由於垃圾收集器已經自動處理場景,因此不需要「特別」標記數組。事實上,一旦數組不再用於聲明方法,數組就可以被垃圾回收。無需等到方法返回。

6

是的,一個新的數組將在每次調用時分配。

除了與內聯方法與params,這是由@PeterDuniho解釋的複雜性,考慮一下:這有params重載所有的關鍵性能.NET方法有重載採取只有一個或幾個參數了。如果自動優化是可能的,他們不會這樣做。

  • Console(也StringTextWriterStringBuilder等):

    • public static void Write(String format, params Object[] arg)
    • public static void Write(String format, Object arg0)
    • public static void Write(String format, Object arg0, Object arg1)
    • public static void Write(bool value)
  • Array

    • public unsafe static Array CreateInstance(Type elementType, params int[] lengths)
    • public unsafe static Array CreateInstance(Type elementType, int length)
    • public unsafe static Array CreateInstance(Type elementType, int length1, int length2)
    • public unsafe static Array CreateInstance(Type elementType, int length1, int length2, int length3)
  • Path

    • public static String Combine(params String[] paths)
    • public static String Combine(String path1, String path2)
    • public static String Combine(String path1, String path2, String path3)
  • CancellationTokenSource

    • public static CancellationTokenSource CreateLinkedTokenSource(params CancellationToken[] tokens)
    • public static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token1, CancellationToken token2)

P. S.我承認這一點並不能證明什麼,因爲OPTI可能已經在更高版本中引入了mization,但它仍然需要考慮。 CancellationTokenSource在最近的4.0版本中引入。

+0

同意這些是性能優化。但它不一定與數組的創建有關。爲大多數常見情況編寫優化算法可能很有用。例如。一個參數可能很容易處理,而n個參數需要循環,動態內存使用等。 –

2

編譯器當前在方法調用之前創建一個新對象。這不是必需的,JITter可能會優化它。

請參閱https://github.com/dotnet/roslyn/issues/36關於可能改變性能改進的討論。

+0

C#編譯器需要在C#版本1.0到6.0(意味着所有現有版本的C#)下創建一個新數組。抖動原則上可以做出神奇的事情,但在這種情況下它不會。您的鏈接提供了一些有趣的討論,討論一些新的'params'功能,它們可能會或可能不會被添加到未來版本的C#中。我們可以設想一些方法來在調用堆棧上保留變量長度的「參數列表」,而不必在堆上分配一個新的對象(在許多_typical_用例中這將是短暫的)。但是,這不是在C#和CLR中存在的東西。 –

+0

需要做一些等同於創建新數組的事情,但實際上並不需要創建一個新數組。例如,它理論上可以預先分配數組以便在循環中使用。靜態分析可能會導致在循環之外創建單個數組,併爲每個調用重新使用數組。 – jmoreno

+0

如果每次使用相同參數再次調用該方法時創建新數組,則完成該操作,但接收數組的方法可能會注意到不同之處。 C#語言規範說將創建一個新的數組。因此,由於C#編譯器無法預見並查看接收方法是否真的會檢查它是否爲新實例,因此C#編譯器在進行任何「優化」時都會遇到困難。運行時和抖動原則上可能會作弊,但據我所知,它不會(在這種情況下)。查看我的答案,瞭解揭示「作弊」的方法示例。 –

相關問題