言
說到函數式編程,大家可能第一印象都是學院派的那些晦澀難懂的代碼,充滿了一大堆抽象的不知所云的符號,似乎只有大學里的計算機教授才會使用這些東西。在曾經的某個時代可能確實如此,但是近年來隨著技術的發展,函數式編程已經在實際生產中發揮巨大的作用了,越來越多的語言開始加入閉包,匿名函數等非常典型的函數式編程的特性,從某種程度上來講,函數式編程正在逐步“同化”命令式編程。
JavaScript 作為一種典型的多范式編程語言,這兩年隨著React的火熱,函數式編程的概念也開始流行起來,RxJS、cycleJS、lodashJS、underscoreJS等多種開源庫都使用了函數式的特性。所以下面介紹一些函數式編程的知識和概念。
純函數
如果你還記得一些初中的數學知識的話,函數 f 的概念就是,對于輸入 x 產生一個輸出 y = f(x)。這便是一種最簡單的純函數。純函數的定義是,對于相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,也不依賴外部環境的狀態。
下面來舉個栗子,比如在Javascript中對于數組的操作,有些是純的,有些就不是純的:
在函數式編程中,我們想要的是 slice 這樣的純函數,而不是 splice這種每次調用后都會把數據弄得一團亂的函數。
為什么函數式編程會排斥不純的函數呢?下面再看一個例子:
//不純的 var min = 18; var checkage = age => age > min; //純的,這很函數式 var checkage = age => age > 18;
在不純的版本中,checkage 這個函數的行為不僅取決于輸入的參數 age,還取決于一個外部的變量 min,換句話說,這個函數的行為需要由外部的系統環境決定。對于大型系統來說,這種對于外部狀態的依賴是造成系統復雜性大大提高的主要原因。
可以注意到,純的 checkage 把關鍵數字 18 硬編碼在函數內部,擴展性比較差,我們可以在后面的柯里化中看到如何用優雅的函數式解決這種問題。
純函數不僅可以有效降低系統的復雜度,還有很多很棒的特性,比如可緩存性:
import _ from 'lodash'; var sin = _.memorize(x => Math.sin(x)); //第一次計算的時候會稍慢一點 var a = sin(1); //第二次有了緩存,速度極快 var b = sin(1);
函數的柯里化
函數柯里化(curry)的定義很簡單:傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
比如對于加法函數 var add = (x, y) => x + y ,我們可以這樣進行柯里化:
//比較容易讀懂的ES5寫法 var add = function(x){ return function(y){ return x + y } } //ES6寫法,也是比較正統的函數式寫法 var add = x => (y => x + y); //試試看 var add2 = add(2); var add200 = add(200); add2(2); // =>4 add200(50); // =>250
對于加法這種極其簡單的函數來說,柯里化并沒有什么大用處。
還記得上面那個 checkage 的函數嗎?我們可以這樣柯里化它:
var checkage = min => (age => age > min); var checkage18 = checkage(18); checkage18(20); // =>true
事實上柯里化是一種“預加載”函數的方法,通過傳遞較少的參數,得到一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的“緩存”,是一種非常高效的編寫函數的方法:
函數組合
學會了使用純函數以及如何把它柯里化之后,我們會很容易寫出這樣的“包菜式”代碼:
h(g(f(x)));
雖然這也是函數式的代碼,但它依然存在某種意義上的“不優雅”。為了解決函數嵌套的問題,我們需要用到“函數組合”:
//兩個函數的組合 var compose = function(f, g) { return function(x) { return f(g(x)); }; }; //或者 var compose = (f, g) => (x => f(g(x))); var add1 = x => x + 1; var mul5 = x => x * 5; compose(mul5, add1)(2); // =>15
我們定義的compose就像雙面膠一樣,可以把任何兩個純函數結合到一起。當然你也可以擴展出組合三個函數的“三面膠”,甚至“四面膠”“N面膠”。
這種靈活的組合可以讓我們像拼積木一樣來組合函數式的代碼:
var first = arr => arr[0]; var reverse = arr => arr.reverse(); var last = compose(first, reverse); last([1,2,3,4,5]); // =>5
Point Free
有了柯里化和函數組合的基礎知識,下面介紹一下Point Free這種代碼風格。
細心的話你可能會注意到,之前的代碼中我們總是喜歡把一些對象自帶的方法轉化成純函數:
var map = (f, arr) => arr.map(f); var toUpperCase = word => word.toUpperCase();
這種做法是有原因的。
Point Free這種模式現在還暫且沒有中文的翻譯,有興趣的話可以看看這里的英文解釋:
https://en.wikipedia.org/wiki/Tacit_programming
用中文解釋的話大概就是,不要命名轉瞬即逝的中間變量,比如:
//這不Piont free var f = str => str.toUpperCase().split(' ');
這個函數中,我們使用了 str 作為我們的中間變量,但這個中間變量除了讓代碼變得長了一點以外是毫無意義的。下面改造一下這段代碼:
var toUpperCase = word => word.toUpperCase(); var split = x => (str => str.split(x)); var f = compose(split(' '), toUpperCase); f("abcd efgh"); // =>["ABCD", "EFGH"]
這種風格能夠幫助我們減少不必要的命名,讓代碼保持簡潔和通用。當然,為了在一些函數中寫出Point Free的風格,在代碼的其它地方必然是不那么Point Free的,這個地方需要自己取舍。
聲明式與命令式代碼
命令式代碼的意思就是,我們通過編寫一條又一條指令去讓計算機執行一些動作,這其中一般都會涉及到很多繁雜的細節。
而聲明式就要優雅很多了,我們通過寫表達式的方式來聲明我們想干什么,而不是通過一步一步的指示。
//命令式 var CEOs = []; for(var i = 0; i < companies.length; i++){ CEOs.push(companies[i].CEO) } //聲明式 var CEOs = companies.map(c => c.CEO);
命令式的寫法要先實例化一個數組,然后再對 companies 數組進行for循環遍歷,手動命名、判斷、增加計數器,就好像你開了一輛零件全部暴露在外的汽車一樣,雖然很機械朋克風,但這并不是優雅的程序員應該做的。
聲明式的寫法是一個表達式,如何進行計數器迭代,返回的數組如何收集,這些細節都隱藏了起來。它指明的是做什么,而不是怎么做。除了更加清晰和簡潔之外,map 函數還可以進一步獨立優化,甚至用解釋器內置的速度極快的 map 函數,這么一來我們主要的業務代碼就無須改動了。
函數式編程的一個明顯的好處就是這種聲明式的代碼,對于無副作用的純函數,我們完全可以不考慮函數內部是如何實現的,專注于編寫業務代碼。優化代碼時,目光只需要集中在這些穩定堅固的函數內部即可。
相反,不純的不函數式的代碼會產生副作用或者依賴外部系統環境,使用它們的時候總是要考慮這些不干凈的副作用。在復雜的系統中,這對于程序員的心智來說是極大的負擔。
尾聲
任何代碼都是要有實際用處才有意義,對于JS來說也是如此。然而現實的編程世界顯然不如范例中的函數式世界那么美好,實際應用中的JS是要接觸到ajax、DOM操作,NodeJS環境中讀寫文件、網絡操作這些對于外部環境強依賴,有明顯副作用的“很臟”的工作。
參考
https://zhuanlan.zhihu.com/p/21714695
文共6287字,預計學習時長20分鐘或更長
圖片來源:Irvan Smith / Unsplash
人們認為JavaScript是最適合初學者的語言。一部分原因在于JavaScript在互聯網中運用廣泛,另一部分原因在于其自身特性使得即使編寫的代碼不那么完美依然可以運行:無論是否少了一個分號或是內存管理問題,它都不像許多其他語言那樣嚴格,但在開始學習之前,要確保你已經知道JavaScript的來龍去脈,包括可以自動完成的事情和“幕后”的操作。
本文將介紹一些面試時關于JavaScript的常見問題,以及一些突發難題。當然,每次面試都是不同的,你也可能不會遇見這類問題。但是知道的越多,準備的就越充分。
如果在面試中突然問到下列問題,似乎很難回答。即便如此,這些問題在準備中仍發揮作用:它們揭示了JavaScript的一些有趣的功能,并強調在提出編程語言時,首先必須做出的一些決定。
了解有關JavaScript的更多功能,建議訪問https://wtfjs.com。
1. 為什么Math.max()小于Math.min()?
Math.max()> Math.min()輸出錯誤這一說法看上去有問題,但其實相當合理。
如果沒有給出參數,Math.min()返回infinity(無窮大),Math.max()返回-infinity(無窮小)。這只是max()和min()方法規范的一部分,但選擇背后的邏輯值得深議。了解其中原因,請看以下代碼:
Math.min(1) // 1 Math.min(1, infinity)// 1 Math.min(1, -infinity)// -infinity
如果-infinity(無窮?。┳鳛镸ath.min()的默認參數,那么每個結果都是-infinity(無窮小),這毫無用處! 然而,如果默認參數是infinity(無窮大),則無論添加任何參數返回都會是該數字 - 這就是我們想要的運行方式。
2. 為什么0.1+0.2不等于0.3
簡而言之,這與JavaScript在二進制中存儲浮點數的準確程度有關。在Google Chrome控制臺中輸入以下公式將得到:
0.1 + 0.2// 0.30000000000000004 0.1 + 0.2 - 0.2// 0.10000000000000003 0.1 + 0.7// 0.7999999999999999
如果是簡單的等式,對準確度沒有要求,這不太可能產生問題。但是如果需要測試相等性,即使是簡單地應用也會導致令人頭疼的問題。解決這些問題,有以下幾種方案。
Fixed Point固定點
例如,如果知道所需的最大精度(例如,如果正在處理貨幣),則可以使用整數類型來存儲該值。因此,可以存儲499而非4.99美元,并在此基礎上執行任何等式,然后可以使用類似result =(value / 100).toFixed(2)的表達式將結果顯示給最終用戶,該表達式返回一個字符串。
BCD代碼
如果精度非常重要,另一種方法是使用二進制編碼的十進制(BCD)格式,可以使用BCD庫(https://formats.kaitai.io/bcd/javascript.html)訪問JavaScript。每個十進制值分別存儲在一個字節(8位)中。鑒于一個字節可以存儲16個單獨值,而該系統僅使用0-9位,所以這種方法效率低下。但是,如果十分注重精確度,采用何種方法都值得考量。
3. 為什么018減017等于3?
018-017返回3實際是靜默類型轉換的結果。這種情況,討論的是八進制數。
八進制數簡介
你或許知道計算中使用二進制(base-2)和十六進制(base-16)數字系統,但是八進制(base-8)在計算機歷史中的地位也舉足親重:在20世紀50年代后期和 20世紀60年代間,八進制被用于簡化二進制,削減高昂的制造系統中的材料成本。
不久以后Hexadecimal(十六進制)開始登上歷史舞臺:
1965年發布的IBM360邁出了從八進制到十六進制的決定性一步。我們這些習慣八進制的人對這一舉措感到震驚!沃恩·普拉特(Vaughan Pratt)
如今的八進制數
但在現代編程語言中,八進制又有何作用呢?針對某些案例,八進制比十六進制更具優勢,因為它不需要任何非數字(使用0-7而不是0-F)。
一個常見用途是Unix系統的文件權限,其中有八個權限變體:
4 2 1
0 - - - no permissions
1 - - x only execute
2 - x - only write
3 - x x write and execute
4 x - - only read
5 x - x read and execute
6 x x - read and write
7 x x x read, write and execute
出于相似的原由,八進制也用于數字顯示器。
回到問題本身
在JavaScript中,前綴0將所有數字轉換為八進制。但是,八進制中不使用數字8,任何包含8的數字都將自動轉換為常規十進制數。
因此,018-017實際上等同于十進制表達式:18-15,因為017使用八進制而018使用十進制。
圖片來源:pexels.com/@divinetechygirl
本節中,將介紹面試中一些更加常見的JavaScript問題。第一次學習JavaScript時,這些問題容易被忽略。但在編寫最佳代碼時,了解下述問題用處頗大。
4. 函數表達式與函數聲明有哪些不同?
函數聲明使用關鍵字function,后跟函數的名稱。相反,函數表達式以var,let或const開頭,后跟函數名稱和賦值運算符=。請看以下代碼:
// Function Declaration function sum(x, y) { return x + y; }; // Function Expression: ES5 var sum = function(x, y) { return x + y; }; // Function Expression: ES6+ const sum = (x, y) => { return x + y };
實際操作中,關鍵的區別在于函數聲明要被提升,而函數表達式則沒有。這意味著JavaScript解釋器將函數聲明移動到其作用域的頂部,因此可以定義函數聲明并在代碼中的任何位置調用它。相比之下,只能以線性順序調用函數表達式:必須在調用它之前解釋。
如今,許多開發人員偏愛函數表達式有如下幾個原因:
· 首先,函數表達式實施更加可預測的結構化代碼庫。當然,函數聲明也可使用結構化代碼庫; 只是函數聲明讓你更容易擺脫凌亂的代碼。
· 其次,可以將ES6語法用于函數表達式:這通常更為簡潔,let和const可以更好地控制是否重新賦值變量,我們將在下一個問題中看到。
5. var,let和const有什么區別?
自ES6發布以來,現代語法已進入各行各業,這已是一個極其常見的面試問題。Var是第一版JavaScript中的變量聲明關鍵字。但它的缺點導致在ES6中采用了兩個新關鍵字:let和const。
這三個關鍵字具有不同的分配,提升和域 - 因此我們將單獨討論。
i) 分配
最基本的區別是let和var可以重新分配,而const則不能。這使得const成為不變變量的最佳選擇,并且它將防止諸如意外重新分配之類的失誤。注意,當變量表示數組或對象時,const確實允許變量改變,只是無法重新分配變量本身。
Let 和var都可重新分配,但是正如以下幾點應該明確的那樣,如果不是所有情況都要求更改變量,多數選擇中,let具有優于var的顯著優勢。
ii)提升
與函數聲明和表達式(如上所述)之間的差異類似,使用var聲明的變量總是被提升到它們各自的頂部,而使用const和let聲明的變量被提升,但是如果你試圖在聲明之前訪問,將會得到一個TDZ(時間死區)錯誤。由于var可能更容易出錯,例如意外重新分配,因此運算是有用的。請看以下代碼:
var x = "global scope"; function foo() { var x = "functional scope"; console.log(x); } foo(); // "functional scope" console.log(x); // "global scope"
這里,foo()和console.log(x)的結果與預期一致。但是,如果去掉第二個變量又會發生什么呢?
var x = "global scope"; function foo() { x = "functional scope"; console.log(x); } foo(); // "functional scope" console.log(x); // "functional scope"
盡管在函數內定義,但x =“functional scope”已覆蓋全局變量。需要重復關鍵字var來指定第二個變量x僅限于foo()。
iii) 域
雖然var是function-scoped(函數作用域),但let和const是block-scoped(塊作用域的:一般情況下,Block是大括號{}內的任何代碼,包括函數,條件語句和循環。為了闡明差異,請看以下代碼:
var a = 0; let b = 0; const c = 0; if (true) { var a = 1; let b = 1; const c = 1; } console.log(a); // 1 console.log(b); // 0 console.log(c); // 0
在條件塊中,全局范圍的var a已重新定義,但全局范圍的let b和const c則沒有。一般而言,確保本地任務保持在本地執行,將使代碼更加清晰,減少出錯。
6. 如果分配不帶關鍵字的變量會發生什么?
如果不使用關鍵字定義變量,又會如何?從技術上講,如果x尚未定義,則x = 1是window.x = 1的簡寫。
要想完全杜絕這種簡寫,可以編寫嚴格模式,——在ES5中介紹過——在文檔頂部或特定函數中寫use strict。后,當你嘗試聲明沒有關鍵字的變量時,你將收到一條報語法錯誤:Uncaught SyntaxError:Unexpected indentifier。
7. 面向對象編程(OOP)和函數式編程(FP)之間的區別是什么?
JavaScript是一種多范式語言,即它支持多種不同的編程風格,包括事件驅動,函數和面向對象。
編程范式各有不同,但在當代計算中,函數編程和面向對象編程最為流行 - 而JavaScript兩種都可執行。
面向對象編程
OOP以“對象”這一概念為基礎的數據結構,包含數據字段(JavaScript稱為類)和程序(JavaScript中的方法)。
一些JavaScript的內置對象包括Math(用于random,max和sin等方法),JSON(用于解析JSON數據)和原始數據類型,如String,Array,Number和Boolean。
無論何時采用的內置方法,原型或類,本質上都在使用面向對象編程。
函數編程
FP(函數編程)以“純函數”的概念為基礎,避免共享狀態,可變數據和副作用。這可能看起來像很多術語,但可能已經在代碼中創建了許多純函數。
輸入相同數據,純函數總是返回相同的輸出。這種方式沒有副作用:除了返回結果之外,例如登錄控制臺或修改外部變量等都不會發生。
至于共享狀態,這里有一個簡單的例子,即使輸入是相同的,狀態仍可以改變函數的輸出。設置一個具有兩個函數的代碼:一個將數字加5,另一個將數字乘以5。
const num = { val: 1 }; const add5 = () => num.val += 5; const multiply5 = () => num.val *= 5;
如果先調用add5在調用乘以5,則整體結果為30。但是如果以相反的方式執行函數并記錄結果,則輸出為10,與之前結果不一致。
這違背了函數式編程的原理,因為函數的結果因Context調用方法而異。 重新編寫上面的代碼,以便結果更易預測:
const num = { val: 1 }; const add5 = () => Object.assign({}, num, {val: num.val + 5}); const multiply5 = () => Object.assign({}, num, {val: num.val * 5});
現在,num.val的值仍然為1,無論Context調用的方法如何,add5(num)和multiply5(num)將始終輸出相同的結果。
8. 命令式和聲明性編程之間有什么區別?
關于命令式編程和聲明式編程的區別,可以以OOP(面向對象編程)和FP(函數式編程)為參考。
這兩種是描述多種不同編程范式共有特征的概括性術語。FP(函數式編程)是聲明性編程的一個范例,而OOP(面向對象編程)是命令式編程的一個范例。
從基本的意義層面,命令式編程關注的是如何做某事。它以最基本的方式闡明了步驟,并以for和while循環,if和switch陳述句等為特征。
const sumArray = array => { let result = 0; for (let i = 0; i < array.length; i++) { result += array[i] }; return result; }
相比之下,聲明性編程關注的是做什么,它通過依賴表達式將怎樣做抽出來。這通常會產生更簡潔的代碼,但是在規模上,由于透明度低,調試會更加困難。
這是上述的sumArray()函數的聲明方法。
const sumArray = array => { return array.reduce((x, y) => x + y) };
圖片來源:pexels.com/@rawpixel
9. 是什么基于原型的繼承?
最后,要講到的是基于原型的繼承。面向對象編程有幾種不同的類型,JavaScript使用的是基于原型的繼承。該系統通過使用現有對象作為原型,允許重復運行。
即使是首次遇到原型這一概念,使用內置方法時也會遇到原型系統。 例如,用于操作數組的函數(如map,reduce,splice等)都是Array.prototype對象的方法。實際上,數組的每個實例(使用方括號[]定義,或者 -不常見的 new Array())都繼承自Array.prototype,這就是為什么map,reduce和spliceare等方法都默認可用的原因。
幾乎所有內置對象都是如此,例如字符串和布爾運算:只有少數,如Infinity,NaN,null和undefined等沒有類或方法。
在原型鏈的末尾,能發現 Object.prototype,幾乎JavaScript中的每個對象都是Object的一個實例。比如Array. prototype和String. prototype都繼承了Object.prototype的類和方法。
要想對使用prototype syntax的對象添加類和方法,只需將對象作為函數啟動,并使用prototype關鍵字添加類和方法:
function Person() {}; Person.prototype.forename = "John"; Person.prototype.surname = "Smith";
是否應該覆蓋或擴展原型運算?
可以使用與創建擴展prototypes同樣的方式改變內置運算,但是大多數開發人員(以及大多數公司)不會建議這樣做。
如果希望多個對象進行同樣的運算,可以創建一個自定義對象(或定義你自己的“類”或“子類”),這些對象繼承內置原型而不改變原型本身。如果打算與其他開發人員合作,他們對JavaScript的默認行為有一定的預期,編輯此默認行為很容易導致出錯。
總的來說,這些問題能夠幫助你更好理解JavaScript,包括其核心功能和其他鮮為人知的功能 ,并且望能助你為下次的面試做好準備。
留言 點贊 關注
我們一起分享AI學習與發展的干貨
歡迎關注全平臺AI垂類自媒體 “讀芯術”
迎使用.NET 6。今天的版本是.NET 團隊和社區一年多努力的結果。C# 10 和F# 6 提供了語言改進,使您的代碼更簡單、更好。性能大幅提升,我們已經看到微軟降低了托管云服務的成本。.NET 6 是第一個原生支持Apple Silicon (Arm64) 的版本,并且還針對Windows Arm64 進行了改進。我們構建了一個新的動態配置文件引導優化(PGO) 系統,該系統可提供僅在運行時才可能進行的深度優化。使用dotnet monitor和OpenTelemetry改進了云診斷。WebAssembly支持更有能力和性能。為HTTP/3添加了新的API ,處理JSON、數學和直接操作內存。.NET 6 將支持三年。開發人員已經開始將應用程序升級到.NET 6,我們在生產中聽到了很好的早期成果。.NET 6 已為您的應用程序做好準備。
您可以下載適用于Linux、macOS 和Windows 的.NET 6 。
請參閱ASP.NET Core、Entity Framework、Windows Forms、.NET MAUI、YARP和dotnet 監視器帖子,了解各種場景中的新增功能。
.NET 6 是:
該版本包括大約一萬次git 提交。即使這篇文章很長,它也跳過了許多改進。您必須下載并試用.NET 6 才能看到所有新功能。
.NET 6 是一個長期支持(LTS) 版本,將支持三年。它支持多種操作系統,包括macOS Apple Silicon 和Windows Arm64。
Red Hat與.NET 團隊合作,在Red Hat Enterprise Linux 上支持.NET。在RHEL 8 及更高版本上,.NET 6 將可用于AMD 和Intel (x64_64)、ARM (aarch64) 以及IBM Z 和LinuxONE (s390x) 架構。
請開始將您的應用程序遷移到.NET 6,尤其是.NET 5 應用程序。我們從早期采用者那里聽說,從.NET Core 3.1 和.NET 5 升級到.NET 6 很簡單。
Visual Studio 2022和Visual Studio 2022 for Mac支持.NET 6 。Visual Studio 2019、Visual Studio for Mac 8 或MSBuild 16 不支持它。如果要使用.NET 6,則需要升級到Visual Studio 2022(現在也是64 位)。Visual Studio Code C# 擴展支持.NET 6 。
Azure App 服務:
注意:如果您的應用已經在應用服務上運行.NET 6 預覽版或RC 版本,則在將.NET 6 運行時和SDK 部署到您所在區域后,它將在第一次重新啟動時自動更新。如果您部署了一個獨立的應用程序,您將需要重新構建和重新部署。
.NET 6 為瀏覽器、云、桌面、物聯網和移動應用程序提供了一個統一的平臺。底層平臺已更新,可滿足所有應用類型的需求,并便于在所有應用中重用代碼。新功能和改進同時適用于所有應用程序,因此您在云或移動設備上運行的代碼的行為方式相同并具有相同的優勢。
.NET 開發人員的范圍隨著每個版本的發布而不斷擴大。機器學習和WebAssembly是最近添加的兩個。例如,通過機器學習,您可以編寫在流數據中查找異常的應用程序。使用WebAssembly,您可以在瀏覽器中托管.NET 應用程序,就像HTML 和JavaScript 一樣,或者將它們與HTML 和JavaScript 混合使用。
最令人興奮的新增功能之一是.NET Multi-platform App UI (.NET MAUI)。您現在可以在單個項目中編寫代碼,從而跨桌面和移動操作系統提供現代客戶端應用程序體驗。.NET MAUI 將比.NET 6 稍晚發布。我們在.NET MAUI 上投入了大量時間和精力,很高興能夠發布它并看到.NET MAUI 應用程序投入生產。
當然,.NET 應用程序也可以在家中使用Windows 桌面(使用Windows Forms和WPF)以及使用ASP.NET Core 在云中。它們是我們提供時間最長的應用程序類型,并且仍然非常受歡迎,我們在.NET 6 中對其進行了改進。
繼續以廣泛平臺為主題,在所有這些操作系統上編寫.NET 代碼很容易。
要以 .NET 6 為目標,您需要使用.NET 6 目標框架,如下所示:
<TargetFramework>net6.0<TargetFramework>
net6.0 Target Framework Moniker (TFM) 使您可以訪問.NET 提供的所有跨平臺API。如果您正在編寫控制臺應用程序、http://ASP.NET Core 應用程序或可重用的跨平臺庫,這是最佳選擇。
如果您針對特定操作系統(例如編寫Windows 窗體或iOS 應用程序),那么還有另一組TFM(每個都針對不言而喻的操作系統)供您使用。它們使您可以訪問所有net6.0的API以及一堆特定于操作系統的API。
每個無版本TFM 都相當于針對.NET 6 支持的最低操作系統版本。如果您想要具體或訪問更新的API,可以指定操作系統版本。
net6.0和net6.0-windows TFMs 都支持(與.NET 5 相同)。Android 和Apple TFM 是.NET 6 的新功能,目前處于預覽階段。稍后的.NET 6 更新將支持它們。
操作系統特定的 TFM 之間沒有兼容性關系。 例如,net6.0-ios與 net6.0-tvos不兼容。 如果您想共享代碼,您需要使用帶有#if 語句的源代碼或帶有net6.0目標代碼的二進制文件來實現。
自從我們啟動.NET Core 項目以來,該團隊一直在不斷地關注性能。Stephen Toub在記錄每個版本的.NET 性能進展方面做得非常出色。歡迎查看在.NET 6 中的性能改進的帖子。在這篇文章中,里面包括您想了解的重大性能改進,包括文件IO、接口轉換、PGO 和System.Text.Json。
動態輪廓引導優化(PGO)可以顯著提高穩態性能。例如,PGO 為TechEmpower JSON"MVC"套件的每秒請求數提高了26%(510K -> 640K)。
動態PGO 建立在分層編譯的基礎上,它使方法能夠首先非常快速地編譯(稱為"第0 層")以提高啟動性能,然后在啟用大量優化的情況下隨后重新編譯(稱為"第1 層")一旦該方法被證明是有影響的。該模型使方法能夠在第0 層中進行檢測,以允許對代碼的執行進行各種觀察。在第1 層重新調整這些方法時,從第0 層執行收集的信息用于更好地優化第1 層代碼。這就是機制的本質。
動態PGO 的啟動時間將比默認運行時稍慢,因為在第0 層方法中運行了額外的代碼來觀察方法行為。
要啟用動態 PGO,請在應用程序將運行的環境中設置 DOTNET_TieredPGO=1。 您還必須確保啟用分層編譯(默認情況下)。 動態 PGO 是可選的,因為它是一種新的且有影響力的技術。 我們希望發布選擇加入使用和相關反饋,以確保它經過全面壓力測試。 我們對分層編譯做了同樣的事情。 至少一個非常大的 Microsoft 服務支持并已在生產中使用動態 PGO。 我們鼓勵您嘗試一下。
您可以在.NET 6中的性能帖子中看到更多關于動態PGO 優勢的信息,包括以下微基準,它測量特定LINQ 枚舉器的成本。
private IEnumerator<long> _source = Enumerable.Range(0, long.MaxValue).GetEnumerator();
[Benchmark]
public void MoveNext() => _source.MoveNext();
這是有和沒有動態PGO 的結果。
這是一個相當大的差異,但代碼大小也有所增加,這可能會讓一些讀者感到驚訝。這是由JIT 生成的匯編代碼的大小,而不是內存分配(這是一個更常見的焦點)。.NET 6 性能帖子對此有很好的解釋。
PGO 實現中常見的一種優化是"熱/冷分離",其中經常執行的方法部分(“熱”)在方法開始時靠近在一起,而不經常執行的方法部分(“冷”)是移到方法的末尾。這樣可以更好地使用指令緩存,并最大限度地減少可能未使用的代碼負載。
作為上下文,接口調度是 .NET 中最昂貴的調用類型。 非虛擬方法調用是最快的,甚至更快的是可以通過內聯消除的調用。 在這種情況下,動態 PGO 為 MoveNext 提供了兩個(替代)調用站點。 第一個 - 熱的 - 是對 Enumerable+RangeIterator.MoveNext的直接調用,另一個 - 冷的 - 是通過 IEnumerator<int>的虛擬接口調用。 如果大多數時候最熱門的人都被叫到,那將是一個巨大的勝利。
這就是魔法。當 JIT 檢測此方法的第 0 層代碼時,包括檢測此接口調度以跟蹤每次調用時 \_source的具體類型。 JIT 發現每次調用都在一個名為 Enumerable+RangeIterator的類型上,這是一個私有類,用于在 Enumerable實現內部實現 Enumerable.Range。因此,對于第 1 層,JIT 已發出檢查以查看 \_source的類型是否為 Enumerable+RangeIterator:如果不是,則跳轉到我們之前強調的執行正常接口調度的冷部分。但如果是 - 基于分析數據,預計絕大多數時間都是這種情況 - 然后它可以繼續直接調用非虛擬化的 Enumerable+RangeIterator.MoveNext方法。不僅如此,它還認為內聯 MoveNext 方法是有利可圖的。最終效果是生成的匯編代碼有點大,但針對預期最常見的確切場景進行了優化。當我們開始構建動態 PGO 時,這些就是我們想要的那種勝利。
動態PGO 將在RyuJIT 部分再次討論。
FileStream幾乎完全用.NET 6 重寫,重點是提高異步文件IO 性能。在Windows 上,實現不再使用阻塞API,并且可以 快幾倍 !我們還改進了所有平臺上的內存使用。在第一次異步操作(通常分配)之后,我們已經使異步操作 免分配 !此外,我們已經使Windows 和Unix 實現不同的邊緣情況的行為統一(這是可能的)。
這種重寫的性能改進使所有操作系統受益。對Windows 的好處是最大的,因為它遠遠落后。macOS 和Linux 用戶也應該會看到顯著FileStream的性能改進。
以下基準將100 MB 寫入新文件。
private byte[] _bytes = new byte[8_000];
[Benchmark]
public async Task Write100MBAsync()
{
using FileStream fs = new("file.txt", FileMode.Create, FileAccess.Write, FileShare.None, 1, FileOptions.Asynchronous);
for (int i = 0; i < 100_000_000 / 8_000; i++)
await fs.WriteAsync(_bytes);
}
在帶有SSD 驅動器的Windows 上,我們觀察到 4倍的加速 和超過 1200倍的分配下降 :
我們還認識到需要更高性能的文件 IO 功能:并發讀取和寫入,以及分散/收集 IO。 針對這些情況,我們為 System.IO.File和 System.IO.RandomAccess類引入了新的 API。
async Task AllOrNothingAsync(string path, IReadOnlyList<ReadOnlyMemory<byte>> buffers)
{
using SafeFileHandle handle = File.OpenHandle(
path, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.Asynchronous,
preallocationSize: buffers.Sum(buffer => buffer.Length)); // hint for the OS to pre-allocate disk space
await RandomAccess.WriteAsync(handle, buffers, fileOffset: 0); // on Linux it's translated to a single sys-call!
}
該示例演示:
預分配大小功能提高了性能,因為寫入操作不需要擴展文件,并且文件不太可能被碎片化。這種方法提高了可靠性,因為寫入操作將不再因空間不足而失敗,因為空間已被保留。Scatter/Gather IO API 減少了寫入數據所需的系統調用次數。
界面鑄造性能提高了16% - 38%。這種改進對于C# 與接口之間的模式匹配特別有用。
這張圖表展示了一個有代表性的基準測試的改進規模。
將.NET 運行時的一部分從C++ 遷移到托管C# 的最大優勢之一是它降低了貢獻的障礙。這包括接口轉換,它作為早期的.NET 6 更改移至C#。.NET 生態系統中懂C# 的人比懂C++ 的人多(而且運行時使用具有挑戰性的C++ 模式)。僅僅能夠閱讀構成運行時的一些代碼是培養以各種形式做出貢獻的信心的重要一步。
歸功于 Ben Adams。
我們為System.Text.Json 添加了一個源代碼生成器,它避免了在運行時進行反射和代碼生成的需要,并且可以在構建時生成最佳序列化代碼。序列化程序通常使用非常保守的技術編寫,因為它們必須如此。但是,如果您閱讀自己的序列化源代碼(使用序列化程序),您可以看到明顯的選擇應該是什么,可以使序列化程序在您的特定情況下更加優化。這正是這個新的源生成器所做的。
除了提高性能和減少內存之外,源代碼生成器還生成最適合裝配修整的代碼。這有助于制作更小的應用程序。
序列化POCO是一種非常常見的場景。使用新的源代碼生成器,我們觀察到序列化速度比我們的基準 快1.6倍。
TechEmpower緩存基準測試平臺或框架對來自數據庫的信息進行內存緩存?;鶞蕼y試的.NET 實現執行緩存數據的JSON 序列化,以便將其作為響應發送到測試工具。
我們觀察到約100K RPS 增益( 增加約40%)。與 MemoryCache 性能改進相結合時,.NET 6 的吞吐量比.NET 5 高50% !
歡迎來到C# 10。C# 10 的一個主要主題是繼續從C# 9 中的頂級語句開始的簡化之旅。新功能從 Program.cs中刪除了更多的儀式,導致程序只有一行。 他們的靈感來自于與沒有 C# 經驗的人(學生、專業開發人員和其他人)交談,并了解什么對他們來說最有效且最直觀。
大多數.NET SDK 模板都已更新,以提供現在可以使用C# 10 實現的更簡單、更簡潔的體驗。我們收到反饋說,有些人不喜歡新模板,因為它們不適合專家,刪除面向對象,刪除在編寫C# 的第一天學習的重要概念,或鼓勵在一個文件中編寫整個程序??陀^地說,這些觀點都不正確。新模型同樣適用于作為專業開發人員的學生。但是,它與.NET 6 之前的C 派生模型不同。
C# 10 中還有其他一些功能和改進,包括記錄結構。
全局using 指令讓您using只需指定一次指令并將其應用于您編譯的每個文件。
以下示例顯示了語法的廣度:
您可以將global using語句放在任何 .cs 文件中,包括在 Program.cs中。
隱式 usings 是一個MSBuild 概念,它會根據SDK自動添加一組指令。例如,控制臺應用程序隱式使用不同于ASP.NET Core。
隱式使用是可選的,并在a 中啟用PropertyGroup:
隱式使用對于現有項目是可選的,但默認包含在新C# 項目中。有關詳細信息,請參閱隱式使用。
文件范圍的命名空間使您能夠聲明整個文件的命名空間,而無需將剩余內容嵌套在{ ...}中. 只允許一個,并且必須在聲明任何類型之前出現。
新語法是單個的一行:
namespace MyNamespace;
class MyClass { ... } // Not indented
這種新語法是三行縮進樣式的替代方案:
namespace MyNamespace
{
class MyClass { ... } // Everything is indented
}
好處是在整個文件位于同一個命名空間中的極其常見的情況下減少縮進。
C# 9 將記錄作為一種特殊的面向值的類形式引入。在C# 10 中,您還可以聲明結構記錄。C# 中的結構已經具有值相等,但記錄結構添加了==運算符和IEquatable<T>的實現,以及基于值的ToString實現:
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
就像記錄類一樣,記錄結構可以是"位置的",這意味著它們有一個主構造函數,它隱式聲明與參數對應的公共成員:
public record structPerson(stringFirstName,stringLastName);
但是,與記錄類不同,隱式公共成員是_可變的自動實現的屬性_。這樣一來,記錄結構就成為了元組的自然成長故事。例如,如果您有一個返回類型(string FirstName, string LastName),并且您希望將其擴展為命名類型,您可以輕松地聲明相應的位置結構記錄并維護可變語義。
如果你想要一個具有只讀屬性的不可變記錄,你可以聲明整個記錄結構readonly(就像你可以其他結構一樣):
publicreadonly record structPerson(stringFirstName,stringLastName);
C# 10 不僅支持記錄結構,還支持_所有_結構以及匿名類型的with表達式:
var updatedPerson = person with{FirstName="Mary"};
F# 6旨在讓F# 更簡單、更高效。這適用于語言設計、庫和工具。我們對F# 6(及更高版本)的目標是消除語言中讓用戶感到驚訝或阻礙學習F# 的極端情況。我們很高興能與F# 社區合作進行這項持續的努力。
新語法task {…}直接創建一個任務并啟動它。這是 F# 6 中最重要的功能之一,它使異步任務更簡單、性能更高,并且與 C# 和其他 .NET 語言的互操作性更強。以前,創建 .NET 任務需要使用async {…}來創建任務并調用Async.StartImmediateAsTask。
該功能task {…}建立在稱為“可恢復代碼”RFC FS-1087的基礎之上??苫謴痛a是一個核心特性,我們希望在未來使用它來構建其他高性能異步和屈服狀態機。
F# 6 還為庫作者添加了其他性能特性,包括InlineIfLambda 和F#活動模式的未裝箱表示。一個特別顯著的性能改進在于列表和數組表達式的編譯,現在它們的速度提高了 4倍 ,并且調試也更好、更簡單。
F# 6 啟用expr[idx]索引語法。到目前為止,F# 一直使用 expr.[idx] 進行索引。刪除點符號是基于第一次使用 F# 用戶的反復反饋,點的使用與他們期望的標準實踐有不必要的差異。在新代碼中,我們建議系統地使用新的expr[idx]索引語法。作為一個社區,我們都應該切換到這種語法。
F# 社區為使 F# 語言在 F# 6 中更加統一做出了重要改進。其中最重要的是消除了 F# 縮進規則中的一些不一致和限制。使 F# 更加統一的其他設計添加包括添加as圖案;在計算表達式中允許“重載自定義操作”(對 DSL 有用);允許_丟棄use綁定并允許%B在輸出中進行二進制格式化。F# 核心庫添加了用于復制和更新列表、數組和序列的新函數,以及其他NativePtr內在函數。自 2.0 起棄用的 F# 的一些舊功能現在會導致錯誤。其中許多更改更好地使 F# 與您的期望保持一致,從而減少意外。
F# 6 還增加了對 F# 中其他“隱式”和“類型導向”轉換的支持。這意味著更少的顯式向上轉換,并為 .NET 樣式的隱式轉換添加了一流的支持。F# 也進行了調整,以更好地適應使用 64 位整數的數字庫時代,并隱式擴展了 32 位整數。
F# 6 中的工具改進使日常編碼更容易。新的"管道調試"允許您單步執行、設置斷點并檢查 F# 管道語法input |> f1 |> f2 的中間值。陰影值的調試顯示已得到改進,消除了調試時常見的混淆源。F# 工具現在也更高效,F# 編譯器并行執行解析階段。F# IDE 工具也得到了改進。F# 腳本現在更加健壯,允許您通過global.json文件固定使用的 .NET SDK 版本。
Hot Reload 是另一個性能特性,專注于開發人員的生產力。它使您能夠對正在運行的應用程序進行各種代碼編輯,從而縮短您等待應用程序重新構建、重新啟動或重新導航到您在進行代碼更改后所在位置所需的時間。
Hot Reload 可通過dotnet watch CLI 工具和 Visual Studio 2022 使用。您可以將 Hot Reload 與多種應用類型一起使用,例如 ASP.NET Core、Blazor、.NET MAUI、控制臺、Windows 窗體 (WinForms)、WPF、WinUI 3、Azure 函數等。
使用 CLI 時,只需使用 啟動您的 .NET 6 應用程序dotnet watch,進行任何受支持的編輯,然后在保存文件時(如在 Visual Studio Code 中),這些更改將立即應用。如果不支持更改,詳細信息將記錄到命令窗口。
此圖像顯示了一個使用dotnet watch. 我對.cs文件和.cshtml文件進行了編輯(如日志中所述),兩者都應用于代碼并在不到半秒的時間內非??焖俚胤从吃跒g覽器中。
使用 Visual Studio 2022 時,只需啟動您的應用程序,進行支持的更改,然后使用新的"熱重載"按鈕(如下圖所示)應用這些更改。您還可以通過同一按鈕上的下拉菜單選擇在保存時應用更改。使用 Visual Studio 2022 時,熱重載可用于多個 .NET 版本,適用于 .NET 5+、.NET Core 和 .NET Framework。例如,您將能夠對按鈕的OnClickEvent處理程序進行代碼隱藏更改。應用程序的Main方法不支持它。
注意:RuntimeInformation.FrameworkDescription中存在一個錯誤,該錯誤將在該圖像中展示,很快就會修復。
Hot Reload 還與現有的 Edit and Continue 功能(在斷點處停止時)以及用于實時編輯應用程序 UI 的 XAML Hot Reload 協同工作。目前支持 C# 和 Visual Basic 應用程序(不是 F#)。
.NET 6 中的安全性得到了顯著改進。它始終是團隊關注的重點,包括威脅建模、加密和深度防御防御。
在 Linux 上,我們依賴OpenSSL進行所有加密操作,包括 TLS(HTTPS 必需)。在 macOS 和 Windows 上,我們依賴操作系統提供的功能來實現相同的目的。對于每個新版本的 .NET,我們經常需要添加對新版本 OpenSSL 的支持。.NET 6 增加了對OpenSSL 3的支持。
OpenSSL 3 的最大變化是改進的FIPS 140-2模塊和更簡單的許可。
.NET 6 需要 OpenSSL 1.1 或更高版本,并且會更喜歡它可以找到的最高安裝版本的 OpenSSL,直到并包括 v3。在一般情況下,當您使用的 Linux 發行版默認切換到 OpenSSL 3 時,您最有可能開始使用 OpenSSL 3。大多數發行版還沒有這樣做。例如,如果您在 Red Hat 8 或 Ubuntu 20.04 上安裝 .NET 6,您將不會(在撰寫本文時)開始使用 OpenSSL 3。
OpenSSL 3、Windows 10 21H1 和 Windows Server 2022 都支持ChaCha20Poly1305。您可以在.NET 6 中使用這種新的經過身份驗證的加密方案(假設您的環境支持它)。
感謝 Kevin Jones對 ChaCha20Poly1305 的 Linux 支持。
我們還發布了新的運行時安全緩解路線圖。重要的是,您使用的運行時不受教科書攻擊類型的影響。我們正在滿足這一需求。在 .NET 6 中,我們構建了W^X和英特爾控制流強制技術(CET)的初始實現。W^X 完全受支持,默認為 macOS Arm64 啟用,并且可以選擇加入其他環境。CET 是所有環境的選擇加入和預覽。我們希望在 .NET 7 中的所有環境中默認啟用這兩種技術。
這些天來,對于筆記本電腦、云硬件和其他設備來說,Arm64 令人興奮不已。我們對 .NET 團隊感到同樣興奮,并正在盡最大努力跟上這一行業趨勢。我們直接與 Arm Holdings、Apple 和 Microsoft 的工程師合作,以確保我們的實施是正確和優化的,并且我們的計劃保持一致。這些密切的合作伙伴關系對我們幫助很大。
在此之前,我們通過 .NET Core 3.0 和 Arm32 添加了對 Arm64 的初始支持。該團隊在最近的幾個版本中都對 Arm64 進行了重大投資,并且在可預見的未來這將繼續下去。在 .NET 6 中,我們主要關注在 macOS 和 Windows Arm64 操作系統上支持新的 Apple Silicon 芯片和x64 仿真場景。
您可以在 macOS 11+ 和 Windows 11+ Arm64 操作系統上安裝 Arm64 和 x64 版本的 .NET。我們必須做出多種設計選擇和產品更改以確保其奏效。
我們的策略是“親原生架構”。我們建議您始終使用與原生架構相匹配的 SDK,即 macOS 和 Windows Arm64 上的 Arm64 SDK。SDK 是大量的軟件。在 Arm64 芯片上本地運行的性能將比仿真高得多。我們更新了 CLI 以簡化操作。我們永遠不會專注于優化模擬 x64。
默認情況下,如果您dotnet run是帶有 Arm64 SDK 的 .NET 6 應用程序,它將作為 Arm64 運行。您可以使用參數輕松切換到以 x64 運行,例如-adotnet run -a x64. 相同的論點適用于其他 CLI 動詞。有關更多信息,請參閱 適用于macOS 和Windows Arm64 的.NET 6 RC2 更新。
我想確保涵蓋其中的一個微妙之處。當您使用-a x64時,SDK 仍以 Arm64 方式原生運行。.NET SDK 體系結構中存在進程邊界的固定點。在大多數情況下,一個進程必須全是 Arm64 或全是 x64。我正在簡化一點,但 .NET CLI 會等待 SDK 架構中的最后一個進程創建,然后將其作為您請求的芯片架構(如 x64)啟動。這就是您的代碼運行的過程。這樣,作為開發人員,您可以獲得 Arm64 的好處,但您的代碼可以在它需要的過程中運行。這僅在您需要將某些代碼作為 x64 運行時才相關。如果你不這樣做,那么你可以一直以 Arm64 的方式運行所有東西,這很棒。
對于 macOS 和 Windows Arm64,以下是您需要了解的要點:
有關更多完整信息,請參閱.NET 對macOS 和Windows Arm64的支持。
此討論中缺少Linux。它不像macOS 和Windows 那樣支持x64 仿真。因此,這些新的CLI 特性和支持方法并不直接適用于Linux,Linux 也不需要它們。
我們有一個簡單的工具來演示.NET 運行的環境。
C:Usersrich>dotnet tool install -g dotnet-runtimeinfo
You can invoke the tool using the following command: dotnet-runtimeinfo
Tool 'dotnet-runtimeinfo' (version '1.0.5') was successfully installed.
C:Usersrich>dotnet runtimeinfo
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
**.NET information
Version: 6.0.0
FrameworkDescription: .NET 6.0.0-rtm.21522.10
Libraries version: 6.0.0-rtm.21522.10
Libraries hash: 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6
**Environment information
ProcessorCount: 8
OSArchitecture: Arm64
OSDescription: Microsoft Windows 10.0.22494
OSVersion: Microsoft Windows NT 10.0.22494.0
如您所見,該工具在Windows Arm64 上本機運行。我將向您展示http://ASP.NET Core 的樣子。
您可以看到在macOS Arm64 上的體驗是相似的,并且還展示了架構目標。
rich@MacBook-Air app % dotnet --version
6.0.100
rich@MacBook-Air app % dotnet --info | grep RID
RID: osx-arm64
rich@MacBook-Air app % cat Program.cs
using System.Runtime.InteropServices;
using static System.Console;
WriteLine($"Hello, {RuntimeInformation.OSArchitecture} from {RuntimeInformation.FrameworkDescription}!");
rich@MacBook-Air app % dotnet run
Hello, Arm64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app % dotnet run -a x64
Hello, X64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app %
這張圖片展示了Arm64 執行是Arm64 SDK 的默認設置,以及使用-a參數在目標Arm64 和x64 之間切換是多么容易。完全相同的體驗適用于Windows Arm64。
此圖像演示了相同的內容,但使用的是http://ASP.NET Core。我正在使用與您在上圖中看到的相同的.NET 6 Arm64 SDK。
Docker 支持在本機架構和仿真中運行的容器,本機架構是默認的。這看起來很明顯,但當大多數Docker Hub 目錄都是面向x64 時,這可能會讓人感到困惑。您可以使用-platform linux/amd64來請求x64 圖像。
我們僅支持在Arm64 操作系統上運行Linux Arm64 .NET 容器映像。這是因為我們從不支持在QEMU中運行.NET ,這是Docker 用于架構模擬的。看來這可能是由于 QEMU 的限制。
此圖像演示了我們維護的控制臺示例:mcr.microsoft.com/dotnet/samples。 這是一個有趣的示例,因為它包含一些基本邏輯,用于打印您可以使用的CPU 和內存限制信息。我展示的圖像設置了CPU 和內存限制。
自己試試吧:docker run --rm mcr.microsoft.com/dotnet/samples
Apple Silicon 和x64 仿真支持項目非常重要,但是,我們也普遍提高了Arm64 性能。
此圖像演示了將堆棧幀的內容清零的改進,這是一種常見的操作。綠線是新行為,而橙色線是另一個(不太有益的)實驗,兩者都相對于基線有所改善,由藍線表示。對于此測試,越低越好。
.NET 6 更適合容器,主要基于本文中討論的所有改進,適用于Arm64 和x64。我們還進行了有助于各種場景的關鍵更改。使用.NET 6 驗證容器改進演示了其中一些改進正在一起測試。
Windows 容器改進和新環境變量也包含在11 月9 日(明天)發布的11 月.NET Framework 4.8 容器更新中。
發布說明可在我們的docker 存儲庫中找到:
Windows 容器
.NET 6 增加了對Windows 進程隔離容器的支持。如果您在 Azure Kubernetes 服務(AKS) 中使用Windows 容器,那么您依賴于進程隔離的容器。進程隔離容器可以被認為與Linux 容器非常相似。Linux 容器使用cgroups,Windows 進程隔離容器使用Job Objects。Windows 還提供Hyper-V 容器,通過更強大的虛擬化提供更大的隔離。Hyper-V 容器的.NET 6 沒有任何變化。
此更改的主要價值是現在Environment.ProcessorCount將使用Windows 進程隔離容器報告正確的值。如果在64 核機器上創建2 核容器,Environment.ProcessorCount將返回2. 在以前的版本中,此屬性將報告機器上的處理器總數,與Docker CLI、Kubernetes 或其他容器編排器/運行時指定的限制無關。此值被.NET 的各個部分用于擴展目的,包括.NET 垃圾收集器(盡管它依賴于相關的較低級別的API)。社區庫也依賴此API 進行擴展。
我們最近在AKS 上使用大量pod 在生產中的Windows 容器上與客戶驗證了這一新功能。他們能夠以50% 的內存(與他們的典型配置相比)成功運行,這是以前導致異常的OutOfMemoryException水平StackOverflowException。他們沒有花時間找到最低內存配置,但我們猜測它明顯低于他們典型內存配置的50%。由于這一變化,他們將轉向更便宜的Azure 配置,從而節省資金。只需升級即可,這是一個不錯的、輕松的勝利。
優化縮放
我們從用戶那里聽說,某些應用程序在Environment.ProcessorCount報告正確的值時無法實現最佳擴展。如果這聽起來與您剛剛閱讀的有關Windows 容器的內容相反,那么它有點像。.NET 6 現在提供DOTNET_PROCESSOR_COUNT 環境變量來手動控制Environment.ProcessorCount的值。在典型的用例中,應用程序可能在64 核機器上配置為4核,并且在8或16核方面擴展得最好。此環境變量可用于啟用該縮放。
這個模型可能看起來很奇怪,其中Environment.ProcessorCount和--cpus(通過Docker CLI)值可能不同。默認情況下,容器運行時面向核心等價物,而不是實際核心。這意味著,當你說你想要4 個核心時,你得到的CPU 時間與4 個核心相當,但你的應用程序可能(理論上)在更多的核心上運行,甚至在短時間內在64 核機器上運行所有64 個核心。這可能使您的應用程序能夠在超過4 個線程上更好地擴展(繼續示例),并且分配更多可能是有益的。這假定線程分配基于 Environment.ProcessorCount的值。如果您選擇設置更高的值,您的應用程序可能會使用更多內存。對于某些工作負載,這是一個簡單的權衡。至少,這是一個您可以測試的新選項。
Linux 和Windows 容器均支持此新功能。
Docker 還提供了一個CPU 組功能,您的應用程序可以關聯到特定的內核。在這種情況下不建議使用此功能,因為應用程序可以訪問的內核數量是具體定義的。我們還看到了將它與Hyper-V 容器一起使用時的一些問題,并且它并不是真正適用于那種隔離模式。
Debian 11 "bullseye"
我們密切關注Linux 發行版的生命周期和發布計劃,并嘗試代表您做出最佳選擇。Debian 是我們用于默認Linux 映像的Linux 發行版。如果您6.0從我們的一個容器存儲庫中提取標簽,您將提取一個Debian 映像(假設您使用的是Linux 容器)。對于每個新的.NET 版本,我們都會考慮是否應該采用新的Debian 版本。
作為一項政策,我們不會為了方便標簽而更改Debian 版本,例如6.0, mid-release。如果我們這樣做了,某些應用程序肯定會崩潰。這意味著,在發布開始時選擇Debian 版本非常重要。此外,這些圖像得到了很多使用,主要是因為它們是"好標簽"的引用。
Debian 和.NET 版本自然不會一起計劃。當我們開始.NET 6 時,我們看到Debian "bullseye" 可能會在2021 年發布。我們決定從發布開始就押注于Bullseye。我們開始使用.NET 6 Preview 1發布基于靶心的容器映像,并決定不再回頭。賭注是.NET 6 版本會輸掉與靶心版本的競爭。到8 月8 日,我們仍然不知道Bullseye 什么時候發貨,距離我們自己的版本發布還有三個月,即11 月8 日。我們不想在預覽版Linux 上發布生產.NET 6,但我們堅持我們會輸掉這場競賽的計劃很晚。
當Debian 11 "bullseye"于8 月14 日發布時,我們感到非常驚喜。我們輸掉了比賽,但贏得了賭注。這意味著默認情況下,.NET 6 用戶從第一天開始就可以獲得最佳和最新的Debian。我們相信Debian 11 和.NET 6 將是許多用戶的絕佳組合。抱歉,克星,我們中了靶心。
較新的發行版在其軟件包提要中包含各種軟件包的較新主要版本,并且通??梢愿斓孬@得CVE 修復。這是對較新內核的補充。新發行版可以更好地為用戶服務。
再往前看,我們很快就會開始計劃對Ubuntu 22.04的支持。Ubuntu是另一個Debian 系列發行版,深受.NET 開發人員的歡迎。我們希望為新的Ubuntu LTS 版本提供當日支持。
向Tianon Gravi 致敬,感謝他們為社區維護Debian 映像并在我們有問題時幫助我們。
Dotnet Monitor
dotnet monitor是容器的重要診斷工具。它作為 sidecar 容器鏡像已經有一段時間了,但處于不受支持的"實驗"狀態。作為.NET 6 的一部分,我們正在發布一個基于.NET 6 的dotnet monitor映像,該映像在生產中得到完全支持。
dotnet monitor已被Azure App Service 用作其http://ASP.NET Core Linux 診斷體驗的實現細節。這是預期的場景之一,建立在dotnet monitor 之上,以提供更高級別和更高價值的體驗。
您現在可以拉取新圖像:
docker pull mcr.microsoft.com/dotnet/monitor:6.0
dotnet monitor使從.NET 進程訪問診斷信息(日志、跟蹤、進程轉儲)變得更加容易。在臺式機上訪問所需的所有診斷信息很容易,但是,這些熟悉的技術在使用容器的生產環境中可能不起作用。dotnet monitor提供了一種統一的方式來收集這些診斷工件,無論是在您的桌面計算機上還是在Kubernetes 集群中運行。收集這些診斷工件有兩種不同的機制:
dotnet monitor為.NET 應用程序提供了一個通用的診斷API,可以使用任何工具在任何地方工作?!巴ㄓ肁PI”不是.NET API,而是您可以調用和查詢的Web API。dotnet monitor包括一個ASP.NET Web 服務器,它直接與.NET 運行時中的診斷服務器交互并公開來自診斷服務器的數據設計dotnet monitor可實現生產中的高性能監控和安全使用,以控制對特權信息的訪問。dotnet monitor通過非Internet 可尋址的unix domain socket與運行時交互——跨越容器邊界。該模型通信模型非常適合此用例。
結構化 JSON 日志
JSON 格式化程序現在是aspnet.NET 6 容器映像中的默認控制臺記錄器。.NET 5 中的默認設置為簡單的控制臺格式化程序。進行此更改是為了使默認配置與依賴機器可讀格式(如JSON)的自動化工具一起使用。
圖像的輸出現在如下所示aspnet:
$ docker run --rm -it -p 8000:80 mcr.microsoft.com/dotnet/samples:aspnetapp
{"EventId":60,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository","Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","State":{"Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","path":"/root/.aspnet/DataProtection-Keys","{OriginalFormat}":"Storing keys in a directory u0027{path}u0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed."}}
{"EventId":35,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager","Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","State":{"Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","KeyId":"86cafacf-ab57-434a-b09c-66a929ae4fd7","{OriginalFormat}":"No XML encryptor configured. Key {KeyId:B} may be persisted to storage in unencrypted form."}}
{"EventId":14,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Now listening on: http://[::]:80","State":{"Message":"Now listening on: http://[::]:80","address":"http://[::]:80","{OriginalFormat}":"Now listening on: {address}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Application started. Press Ctrlu002BC to shut down.","State":{"Message":"Application started. Press Ctrlu002BC to shut down.","{OriginalFormat}":"Application started. Press Ctrlu002BC to shut down."}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Hosting environment: Production","State":{"Message":"Hosting environment: Production","envName":"Production","{OriginalFormat}":"Hosting environment: {envName}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Content root path: /app","State":{"Message":"Content root path: /app","contentRoot":"/app","{OriginalFormat}":"Content root path: {contentRoot}"}}
Logging\_\_Console\_\_FormatterName可以通過設置或取消設置環境變量或通過代碼更改來更改記錄器格式類型(有關更多詳細信息,請參閱控制臺日志格式)。
更改后,您將看到如下輸出(就像.NET 5 一樣):
$ docker run --rm -it -p 8000:80 -e Logging__Console__FormatterName="" mcr.microsoft.com/dotnet/samples:aspnetapp
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
No XML encryptor configured. Key {8d4ddd1d-ccfc-4898-9fe1-3e7403bf23a0} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
注意:此更改不會影響開發人員計算機上的.NET SDK,例如dotnet run.此更改特定于aspnet容器映像。
作為我們關注可觀察性的一部分,我們一直在為最后幾個.NET 版本添加對 OpenTelemetry 的支持。在.NET 6 中,我們添加了對OpenTelemetry Metrics API的支持。通過添加對OpenTelemetry 的支持,您的應用程序可以與其他OpenTelemetry系統無縫互操作。
System.Diagnostics.Metrics是OpenTelemetry Metrics API 規范的.NET 實現。Metrics API 是專門為處理原始測量而設計的,目的是高效、同時地生成這些測量的連續摘要。
API 包括Meter可用于創建儀器對象的類。API 公開了四個工具類:Counter、Histogram、ObservableCounter和,ObservableGauge以支持不同的度量方案。此外,API 公開MeterListener該類以允許收聽儀器記錄的測量值,以用于聚合和分組目的。
OpenTelemetry .NET 實現將被擴展以使用這些新的API,這些API 添加了對Metrics 可觀察性場景的支持。
圖書館測量記錄示例
Meter meter = new Meter("io.opentelemetry.contrib.mongodb", "v1.0");
Counter<int> counter = meter.CreateCounter<int>("Requests");
counter.Add(1);
counter.Add(1, KeyValuePair.Create<string, object>("request", "read"));
聽力示例
MeterListener listener = new MeterListener();
listener.InstrumentPublished = (instrument, meterListener) =>
{
if (instrument.Name == "Requests" && instrument.Meter.Name == "io.opentelemetry.contrib.mongodb")
{
meterListener.EnableMeasurementEvents(instrument, null);
}
};
listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
{
Console.WriteLine($"Instrument: {instrument.Name} has recorded the measurement {measurement}");
});
listener.Start();
我們繼續在 Windows 窗體中進行重要改進。.NET 6 包括更好的控件可訪問性、設置應用程序范圍的默認字體、模板更新等的能力。
可訪問性改進
在此版本中,我們添加了用于CheckedListBox、LinkLabel、Panel、ScrollBar和TabControlTrackBar的UIA 提供程序,它們使講述人等工具和測試自動化能夠與應用程序的元素進行交互。
默認字體
您現在可以使用.Application.SetDefaultFont
voidApplication.SetDefaultFont(Font font)
最小的應用程序
以下是帶有 .NET 6 的最小Windows 窗體應用程序:
class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
}
作為.NET 6 版本的一部分,我們一直在更新大多數模板,使其更加現代和簡約,包括Windows 窗體。我們決定讓Windows 窗體模板更傳統一些,部分原因是需要將[STAThread]屬性應用于應用程序入口點。然而,還有更多的戲劇而不是立即出現在眼前。
ApplicationConfiguration.Initialize()是一個源生成API,它在后臺發出以下調用:
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetDefaultFont(new Font(...));
Application.SetHighDpiMode(HighDpiMode.SystemAware);
這些調用的參數可通過csproj 或props 文件中的MSBuild 屬性進行配置。
Visual Studio 2022 中的Windows 窗體設計器也知道這些屬性(目前它只讀取默認字體),并且可以向您顯示您的應用程序,就像它在運行時一樣:
模板更新
C# 的Windows 窗體模板已更新,以支持新的應用程序引導、global using指令、文件范圍的命名空間和可為空的引用類型。
更多運行時 designers
現在您可以構建通用設計器(例如,報表設計器),因為.NET 6 具有設計器和與設計器相關的基礎架構所缺少的所有部分。有關詳細信息,請參閱此博客文章。
在.NET 6中,已為 Windows 和macOS 啟用內存中單文件應用程序。在.NET 5 中,這種部署類型僅限于 Linux。您現在可以為所有受支持的操作系統發布作為單個文件部署和啟動的單文件二進制文件。單文件應用不再將任何核心運行時程序集提取到臨時目錄。
這種擴展功能基于稱為"超級主機"的構建塊。"apphost" 是在非單文件情況下啟動應用程序的可執行文件,例如myapp.exe或./myapp. Apphost 包含用于查找運行時、加載它并使用該運行時啟動您的應用程序的代碼。Superhost 仍然執行其中一些任務,但使用所有CoreCLR 本機二進制文件的靜態鏈接副本。靜態鏈接是我們用來實現單一文件體驗的方法。本機依賴項(如NuGet 包附帶的)是單文件嵌入的顯著例外。默認情況下,它們不包含在單個文件中。例如,WPF 本機依賴項不是超級主機的一部分,因此會在單文件應用程序之外產生其他文件。您可以使用該設置IncludeNativeLibrariesForSelfExtract嵌入和提取本機依賴項。
靜態分析
我們改進了單文件分析器以允許自定義警告。如果您的API 在單文件發布中不起作用,您現在可以使用[RequiresAssemblyFiles]屬性對其進行標記,如果啟用了分析器,則會出現警告。添加該屬性還將使方法中與單個文件相關的所有警告靜音,因此您可以使用該警告將警告向上傳播到您的公共API。
當 PublishSingleFile 設置為true 時,會自動為exe 項目啟用單文件分析器,但您也可以通過將 EnableSingleFileAnalysis 設置為true 來為任何項目啟用它。 如果您想支持將庫作為單個文件應用程序的一部分,這將很有幫助。
在.NET 5 中,我們為單文件包中行為不同的Assembly.Location和一些其他API添加了警告。
壓縮
單文件包現在支持壓縮,可以通過將屬性設置EnableCompressionInSingleFile為true. 在運行時,文件會根據需要解壓縮到內存中。壓縮可以為某些場景節省大量空間。
讓我們看一下與NuGet 包資源管理器一起使用的單個文件發布(帶壓縮和不帶壓縮)。
無壓縮: 172 MB
壓縮: 71.6 MB
壓縮會顯著增加應用程序的啟動時間,尤其是在Unix 平臺上。Unix 平臺有一個不能用于壓縮的無拷貝快速啟動路徑。您應該在啟用壓縮后測試您的應用程序,看看額外的啟動成本是否可以接受。
單文件調試
目前只能使用平臺調試器(如WinDBG)來調試單文件應用程序。我們正在考慮使用更高版本的Visual Studio 2022 添加Visual Studio 調試。
macOS 上的單文件簽名
單文件應用程序現在滿足macOS 上的Apple 公證和簽名要求。具體更改與我們根據離散文件布局構建單文件應用程序的方式有關。
Apple 開始對macOS Catalina 實施新的簽名和公證要求。我們一直在與Apple 密切合作,以了解需求,并尋找使.NET 等開發平臺能夠在該環境中正常工作的解決方案。我們已經進行了產品更改并記錄了用戶工作流程,以滿足Apple 在最近幾個.NET 版本中的要求。剩下的差距之一是單文件簽名,這是在macOS 上分發.NET 應用程序的要求,包括在macOS 商店中。
該團隊一直致力于為多個版本進行IL 修整。.NET 6 代表了這一旅程向前邁出的重要一步。我們一直在努力使更激進的修剪模式安全且可預測,因此有信心將其設為默認模式。TrimMode=link以前是可選功能,現在是默認功能。
我們有一個三管齊下的修剪策略:
由于使用未注釋反射的應用程序的結果不可靠,修剪之前一直處于預覽狀態。有了修剪警告,體驗現在應該是可預測的。沒有修剪警告的應用程序應該正確修剪并且在運行時觀察到行為沒有變化。目前,只有核心的.NET 庫已經完全注解了修剪,但我們希望看到生態系統注釋修剪并兼容修剪
讓我們使用SDK 工具之一的crossgen來看看這個修剪改進。它可以通過幾個修剪警告進行修剪,crossgen 團隊能夠解決。
首先,讓我們看一下將crossgen 發布為一個獨立的應用程序而無需修剪。它是80 MB(包括.NET 運行時和所有庫)。
然后我們可以嘗試(現在是舊版).NET 5 默認修剪模式,copyused. 結果降至55 MB。
新的.NET 6 默認修剪模式link將獨立文件大小進一步降低到36MB。
我們希望新的link修剪模式能更好地與修剪的期望保持一致:顯著節省和可預測的結果。
默認啟用警告
修剪警告告訴您修剪可能會刪除運行時使用的代碼的地方。這些警告以前默認禁用,因為警告非常嘈雜,主要是由于 .NET 平臺沒有參與修剪作為第一類場景。
我們對大部分 .NET 庫進行了注釋,以便它們產生準確的修剪警告。因此,我們覺得是時候默認啟用修剪警告了。http://ASP.NET Core 和 Windows 桌面運行時庫尚未注釋。我們計劃接下來注釋 http://ASP.NET 服務組件(在 .NET 6 之后)。我們希望看到社區在 .NET 6 發布后對 NuGet 庫進行注釋。
您可以通過設置<SuppressTrimAnalysisWarnings>為true來禁用警告。
更多信息:
與本機 AOT 共享
我們也為Native AOT實驗實現了相同的修剪警告,這應該會以幾乎相同的方式改善 Native AOT 編譯體驗。
數學
我們顯著改進了數學 API。社區中的一些人已經在享受這些改進。
面向性能的 API
System.Math 中添加了面向性能的數學 API。如果底層硬件支持,它們的實現是硬件加速的。
新 API:
新的重載:
性能改進:
大整數性能
改進了從十進制和十六進制字符串中解析 BigIntegers。我們看到了高達89% 的改進,如下圖所示(越低越好)。
感謝約瑟夫·達席爾瓦。
Complex API 現在注釋為 readonly
現在對各種API 進行了注釋,System.Numerics.Complexreadonly以確保不會對readonly值或傳遞的值進行復制in。
歸功于hrrrrustic 。
BitConverter 現在支持浮點到無符號整數位廣播
BitConverter 現在支持DoubleToUInt64Bits, HalfToUInt16Bits, SingleToUInt32Bits, UInt16BitsToHalf, UInt32BitsToSingle, 和UInt64BitsToDouble. 這應該使得在需要時更容易進行浮點位操作。
歸功于Michal Petryka 。
BitOperations 支持附加功能
BitOperations現在支持IsPow2,RoundUpToPowerOf2和提供nint/nuint重載現有函數。
感謝約翰凱利、霍耀源和羅賓林德納。
Vector現在支持C# 9 中添加的原始類型nint和nuint原始類型。例如,此更改應該可以更簡單地使用帶有指針或平臺相關長度類型的SIMD 指令。
Vector現在支持一種Sum方法來簡化計算向量中所有元素的“水平和”的需要。歸功于伊萬茲拉塔諾夫。
Vector現在支持一種通用方法As<TFrom, TTo>來簡化在具體類型未知的通用上下文中處理向量。感謝霍耀源
重載支持Span已添加到Vector2、Vector3和Vector4以改善需要加載或存儲矢量類型時的體驗。
更好地解析標準數字格式
我們改進了標準數字類型的解析器,特別是.ToString和.TryFormatParse。他們現在將理解對精度 >99 位小數的要求,并將為那么多位數提供準確的結果。此外,解析器現在更好地支持方法中的尾隨零。
以下示例演示了之前和之后的行為。
System.Text.Json提供多種高性能API 用于處理JSON 文檔。在過去的幾個版本中,我們添加了新功能,以進一步提高JSON 處理性能并減輕對希望從NewtonSoft.Json遷移的人的阻礙。 此版本包括在該路徑上的繼續,并且在性能方面向前邁出了一大步,特別是在序列化程序源生成器方面。
注意:使用.NET 6 RC1 或更早版本的源代碼生成的應用程序應重新編譯。
幾乎所有.NET 序列化程序的支柱都是反射。反射對于某些場景來說是一種很好的能力,但不能作為高性能云原生應用程序(通常(反)序列化和處理大量JSON 文檔)的基礎。反射是啟動、內存使用和程序集修整的問題。
運行時反射的替代方法是編譯時源代碼生成。在.NET 6 中,我們包含一個新的源代碼生成器作為 System.Text.Json. JSON 源代碼生成器可以與多種方式結合使用JsonSerializer并且可以通過多種方式進行配置。
它可以提供以下好處:
默認情況下,JSON 源生成器為給定的可序列化類型發出序列化邏輯。JsonSerializer通過生成直接使用的源代碼,這提供了比使用現有方法更高的性能Utf8JsonWriter。簡而言之,源代碼生成器提供了一種在編譯時為您提供不同實現的方法,以使運行時體驗更好。
給定一個簡單的類型:
namespace Test
{
internal class JsonMessage
{
public string Message { get; set; }
}
}
源生成器可以配置為為示例JsonMessage類型的實例生成序列化邏輯。請注意,類名JsonContext是任意的。您可以為生成的源使用所需的任何類名。
using System.Text.Json.Serialization;
namespace Test
{
[JsonSerializable(typeof(JsonMessage)]
internal partial class JsonContext : JsonSerializerContext
{
}
}
使用此模式的序列化程序調用可能類似于以下示例。此示例提供了可能的最佳性能。
using MemoryStream ms = new();
using Utf8JsonWriter writer = new(ms);
JsonSerializer.Serialize(jsonMessage, JsonContext.Default.JsonMessage);
writer.Flush();
// Writer contains:
// {"Message":"Hello, world!"}
最快和最優化的源代碼生成模式——基于Utf8JsonWriter——目前僅可用于序列化。Utf8JsonReader根據您的反饋,將來可能會提供對反序列化的類似支持。
源生成器還發出類型元數據初始化邏輯,這也有利于反序列化。JsonMessage要反序列化使用預生成類型元數據的實例,您可以執行以下操作:
JsonSerializer.Deserialize(json, JsonContext.Default.JsonMessage);
您現在可以使用System.Text.Json(反)序列化IAsyncEnumerableJSON 數組。以下示例使用流作為任何異步數據源的表示。源可以是本地計算機上的文件,也可以是數據庫查詢或Web 服務API 調用的結果。
JsonSerializer.SerializeAsync已更新以識別并為IAsyncEnumerable值提供特殊處理。
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
static async IAsyncEnumerable<int> PrintNumbers(int n)
{
for (int i = 0; i < n; i++) yield return i;
}
using Stream stream = Console.OpenStandardOutput();
var data = new { Data = PrintNumbers(3) };
await JsonSerializer.SerializeAsync(stream, data); // prints {"Data":[0,1,2]}
IAsyncEnumerable僅使用異步序列化方法支持值。嘗試使用同步方法進行序列化將導致NotSupportedException被拋出。
流式反序列化需要一個新的 API 來返回IAsyncEnumerable<T>。我們為此添加了JsonSerializer.DeserializeAsyncEnumerable方法,您可以在以下示例中看到。
using System;
using System.IO;
using System.Text;
using System.Text.Json;
var stream = new MemoryStream(Encoding.UTF8.GetBytes("[0,1,2,3,4]"));
await foreach (int item in JsonSerializer.DeserializeAsyncEnumerable<int>(stream))
{
Console.WriteLine(item);
}
此示例將按需反序列化元素,并且在使用特別大的數據流時非常有用。它僅支持從根級JSON 數組讀取,盡管將來可能會根據反饋放寬。
現有DeserializeAsync方法名義上支持IAsyncEnumerable<T>,但在其非流方法簽名的范圍內。它必須將最終結果作為單個值返回,如以下示例所示。
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"{""Data"":[0,1,2,3,4]}"));
var result = await JsonSerializer.DeserializeAsync<MyPoco>(stream);
await foreach (int item in result.Data)
{
Console.WriteLine(item);
}
public class MyPoco
{
public IAsyncEnumerable<int> Data { get; set; }
}
在此示例中,反序列化器將IAsyncEnumerable在返回反序列化對象之前緩沖內存中的所有內容。這是因為反序列化器需要在返回結果之前消耗整個 JSON 值。
可寫JSON DOM 特性為System.Text.Json添加了一個新的簡單且高性能的編程模型。這個新的API 很有吸引力,因為它避免了需要強類型的序列化合約,并且與現有的JsonDocument類型相比,DOM 是可變的。
這個新的 API 有以下好處:
以下示例演示了新的編程模型。
// Parse a JSON object
JsonNode jNode = JsonNode.Parse("{"MyProperty":42}");
int value = (int)jNode["MyProperty"];
Debug.Assert(value == 42);
// or
value = jNode["MyProperty"].GetValue<int>();
Debug.Assert(value == 42);
// Parse a JSON array
jNode = JsonNode.Parse("[10,11,12]");
value = (int)jNode[1];
Debug.Assert(value == 11);
// or
value = jNode[1].GetValue<int>();
Debug.Assert(value == 11);
// Create a new JsonObject using object initializers and array params
var jObject = new JsonObject
{
["MyChildObject"] = new JsonObject
{
["MyProperty"] = "Hello",
["MyArray"] = new JsonArray(10, 11, 12)
}
};
// Obtain the JSON from the new JsonObject
string json = jObject.ToJsonString();
Console.WriteLine(json); // {"MyChildObject":{"MyProperty":"Hello","MyArray":[10,11,12]}}
// Indexers for property names and array elements are supported and can be chained
Debug.Assert(jObject["MyChildObject"]["MyArray"][1].GetValue<int>() == 11);
JsonSerializer(System.Text.Json)現在支持在序列化對象圖時忽略循環的能力。該ReferenceHandler.IgnoreCycles選項具有與Newtonsoft.Json ReferenceLoopHandling.Ignore類似的行為。一個關鍵區別是System.Text.Json 實現用null JSON 標記替換引用循環,而不是忽略對象引用。
您可以在以下示例中看到ReferenceHandler.IgnoreCycles的行為。在這種情況下,該Next屬性被序列化為null,因為否則它會創建一個循環。
class Node
{
public string Description { get; set; }
public object Next { get; set; }
}
void Test()
{
var node = new Node { Description = "Node 1" };
node.Next = node;
var opts = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles };
string json = JsonSerializer.Serialize(node, opts);
Console.WriteLine(json); // Prints {"Description":"Node 1","Next":null}
}
通過源代碼構建,您只需幾個命令即可在您自己的計算機上從源代碼構建.NET SDK 。讓我解釋一下為什么這個項目很重要。
源代碼構建是一個場景,也是我們在發布.NET Core 1.0 之前一直與Red Hat 合作開發的基礎架構。幾年后,我們非常接近于交付它的全自動版本。對于Red Hat Enterprise Linux (RHEL) .NET 用戶來說,這個功能很重要。Red Hat 告訴我們,.NET 已經發展成為其生態系統的重要開發者平臺。好的!
Linux 發行版的黃金標準是使用作為發行版存檔一部分的編譯器和工具鏈構建開源代碼。這適用于.NET 運行時(用C++ 編寫),但不適用于任何用C# 編寫的代碼。對于C# 代碼,我們使用兩遍構建機制來滿足發行版要求。這有點復雜,但了解流程很重要。
Red Hat 使用.NET SDK (#1) 的Microsoft 二進制構建來構建.NET SDK 源代碼,以生成SDK (#2) 的純開源二進制構建。之后,使用這個新版本的SDK (#2) 再次構建相同的SDK 源代碼,以生成可證明的開源SDK (#3)。.NET SDK (#3) 的最終二進制版本隨后可供RHEL 用戶使用。之后,Red Hat 可以使用相同的SDK (#3) 來構建新的.NET 版本,而不再需要使用Microsoft SDK 來構建每月更新。
這個過程可能令人驚訝和困惑。開源發行版需要通過開源工具構建。此模式確保不需要Microsoft 構建的SDK,無論是有意還是無意。作為開發者平臺,包含在發行版中的門檻比僅使用兼容許可證的門檻更高。源代碼構建項目使.NET 能夠滿足該標準。
源代碼構建的可交付成果是源代碼壓縮包。源tarball 包含SDK 的所有源(對于給定版本)。從那里,紅帽(或其他組織)可以構建自己的SDK 版本。Red Hat 政策要求使用內置源工具鏈來生成二進制tar 球,這就是他們使用兩遍方法的原因。但是源代碼構建本身不需要這種兩遍方法。
在Linux 生態系統中,給定組件同時擁有源和二進制包或tarball 是很常見的。我們已經有了可用的二進制tarball,現在也有了源tarball。這使得.NET 與標準組件模式相匹配。
.NET 6 的重大改進是源tarball 現在是我們構建的產品。它過去需要大量的人工來制作,這也導致將源tarball 交付給Red Hat 的延遲很長。雙方都對此不滿意。
在這個項目上,我們與紅帽密切合作五年多。它的成功在很大程度上要歸功于我們有幸與之共事的優秀紅帽工程師的努力。其他發行版和組織已經并將從他們的努力中受益。
附帶說明一下,源代碼構建是朝著可重現構建邁出的一大步,我們也堅信這一點。.NET SDK 和C# 編譯器具有重要的可重現構建功能。
除了已經涵蓋的API 之外,還添加了以下API。
壓縮對于通過網絡傳輸的任何數據都很重要。WebSockets 現在啟用壓縮。我們使用了WebSockets 的擴展permessage-deflate實現,RFC 7692。它允許使用該DEFLATE算法壓縮WebSockets 消息負載。此功能是GitHub 上Networking 的主要用戶請求之一。
與加密一起使用的壓縮可能會導致攻擊,例如CRIME和BREACH。這意味著不能在單個壓縮上下文中將秘密與用戶生成的數據一起發送,否則可以提取該秘密。為了讓用戶注意到這些影響并幫助他們權衡風險,我們將其中一個關鍵API 命名為DangerousDeflateOptions。我們還添加了關閉特定消息壓縮的功能,因此如果用戶想要發送秘密,他們可以在不壓縮的情況下安全地執行此操作。
禁用壓縮時WebSocket的內存占用減少了約27%。
從客戶端啟用壓縮很容易,如下例所示。但是,請記住,服務器可以協商設置,例如請求更小的窗口或完全拒絕壓縮。
var cws = new ClientWebSocket();
cws.Options.DangerousDeflateOptions = new WebSocketDeflateOptions()
{
ClientMaxWindowBits = 10,
ServerMaxWindowBits = 10
};
還添加了對 ASP.NET Core 的 WebSocket 壓縮支持。
歸功于伊萬茲拉塔諾夫。
SOCKS是一種代理服務器實現,可以處理任何TCP 或UDP 流量,使其成為一個非常通用的系統。這是一個長期存在的社區請求,已添加到.NET 6中。
此更改增加了對Socks4、Socks4a 和Socks5 的支持。例如,它可以通過SSH 測試外部連接或連接到 Tor 網絡。
該類WebProxy現在接受socks方案,如以下示例所示。
var handler = new HttpClientHandler
{
Proxy = new WebProxy("socks5://127.0.0.1", 9050)
};
var httpClient = new HttpClient(handler);
歸功于Huo yaoyuan。
Microsoft.Extensions.Hosting — 配置主機選項 API
我們在IHostBuilder 上添加了一個新的ConfigureHostOptions API,以簡化應用程序設置(例如,配置關閉超時):
using HostBuilder host = new()
.ConfigureHostOptions(o =>
{
o.ShutdownTimeout = TimeSpan.FromMinutes(10);
})
.Build();
host.Run();
在.NET 5 中,配置主機選項有點復雜:
using HostBuilder host = new()
.ConfigureServices(services =>
{
services.Configure<HostOptions>(o =>
{
o.ShutdownTimeout = TimeSpan.FromMinutes(10);
});
})
.Build();
host.Run();
Microsoft.Extensions.DependencyInjection — CreateAsyncScope API
CreateAsyncScope創建API是為了處理服務的處置IAsyncDisposable。以前,您可能已經注意到處置IAsyncDisposable服務提供者可能會引發InvalidOperationException異常。
以下示例演示了新模式,CreateAsyncScope用于啟用using語句的安全使用。
await using (var scope = provider.CreateAsyncScope())
{
var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}
以下示例演示了現有的問題案例:
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
await using var provider = new ServiceCollection()
.AddScoped<Foo>()
.BuildServiceProvider();
// This using can throw InvalidOperationException
using (var scope = provider.CreateScope())
{
var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}
class Foo : IAsyncDisposable
{
public ValueTask DisposeAsync() => default;
}
以下模式是先前建議的避免異常的解決方法。不再需要它。
var scope = provider.CreateScope();
var foo = scope.ServiceProvider.GetRequiredService<Foo>();
await ((IAsyncDisposable)scope).DisposeAsync();
感謝Martin Bj?rkstr?m 。
Microsoft.Extensions.Logging — 編譯時源生成器
.NET 6 引入了LoggerMessageAttribute類型。 此屬性是Microsoft.Extensions.Logging命名空間的一部分,使用時,它會源生成高性能日志記錄API。源生成日志支持旨在為現代.NET 應用程序提供高度可用和高性能的日志解決方案。自動生成的源代碼依賴于ILogger接口和LoggerMessage.Define功能。
LoggerMessageAttribute源生成器在用于partial日志記錄方法時觸發。當被觸發時,它要么能夠自動生成partial它正在裝飾的方法的實現,要么生成編譯時診斷,并提供有關正確使用的提示。編譯時日志記錄解決方案在運行時通常比現有的日志記錄方法快得多。它通過最大限度地消除裝箱、臨時分配和副本來實現這一點。
與直接手動使用LoggerMessage.Define API相比,有以下好處:
要使用LoggerMessageAttribute,消費類和方法需要是partial。代碼生成器在編譯時觸發并生成partial方法的實現。
public static partial class Log
{
[LoggerMessage(EventId = 0, Level = LogLevel.Critical, Message = "Could not open socket to `{hostName}`")]
public static partial void CouldNotOpenSocket(ILogger logger, string hostName);
}
在前面的示例中,日志記錄方法是static,并且在屬性定義中指定了日志級別。在靜態上下文中使用屬性時,ILogger需要實例作為參數。您也可以選擇在非靜態上下文中使用該屬性。有關更多示例和使用場景,請訪問編譯時日志記錄源生成器文檔。
System.Linq — 可枚舉的支持 Index 和 Range 參數
該Enumerable.ElementAt方法現在接受來自可枚舉末尾的索引,如以下示例所示。
Enumerable.Range(1, 10).ElementAt(^2); // returns 9
添加了一個Enumerable.Take接受Range參數的重載。它簡化了對可枚舉序列的切片:
感謝@dixin 。
System.Linq — TryGetNonEnumeratedCount
該TryGetNonEnumeratedCount方法嘗試在不強制枚舉的情況下獲取源可枚舉的計數。這種方法在枚舉之前預分配緩沖區很有用的場景中很有用,如下面的示例所示。
List<T> buffer = source.TryGetNonEnumeratedCount(out int count) ? new List<T>(capacity: count) : new List<T>();
foreach (T item in source)
{
buffer.Add(item);
}
TryGetNonEnumeratedCount檢查實現ICollection/ ICollection<T>;或利用Linq 采用的一些內部優化的源。
System.Linq — DistinctBy / UnionBy / IntersectBy / ExceptBy
新變體已添加到允許使用鍵選擇器函數指定相等性的集合操作中,如下例所示。
Enumerable.Range(1, 20).DistinctBy(x => x % 3); // {1, 2, 3}
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) };
first.UnionBy(second, person => person.Age); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40), ("Drew", 33) }
System.Linq - MaxBy / MinBy
MaxBy和MinBy方法允許使用鍵選擇器查找最大或最小元素,如下例所示。
var people = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
people.MaxBy(person => person.Age); // ("Ashley", 40)
System.Linq — Chunk
Chunk可用于將可枚舉的源分塊為固定大小的切片,如下例所示。
IEnumerable<int[]> chunks = Enumerable.Range(0, 10).Chunk(size: 3); // { {0,1,2}, {3,4,5}, {6,7,8}, {9} }
歸功于羅伯特安德森。
System.Linq—— // FirstOrDefault 采用默認參數的重載 LastOrDefaultSingleOrDefault
如果源可枚舉為空,則現有的FirstOrDefault /LastOrDefault /SingleOrDefault方法返回default(T)。添加了新的重載,它們接受在這種情況下返回的默認參數,如以下示例所示。
Enumerable.Empty\<int\>().SingleOrDefault(-1); // returns -1
感謝@ Foxtrek64 。
System.Linq — Zip 接受三個可枚舉的重載
Zip方法現在支持組合三個枚舉,如以下示例所示。
var xs = Enumerable.Range(1, 10);
var ys = xs.Select(x => x.ToString());
var zs = xs.Select(x => x % 2 == 0);
foreach ((int x, string y, bool z) in Enumerable.Zip(xs,ys,zs))
{
}
歸功于Huo yaoyuan。
優先隊列
PriorityQueue<TElement, TPriority>(System.Collections.Generic) 是一個新集合,可以添加具有值和優先級的新項目。在出隊時,PriorityQueue 返回具有最低優先級值的元素。您可以認為這個新集合類似于Queue<T>但每個入隊元素都有一個影響出隊行為的優先級值。
以下示例演示了.PriorityQueue<string, int>
// creates a priority queue of strings with integer priorities
var pq = new PriorityQueue<string, int>();
// enqueue elements with associated priorities
pq.Enqueue("A", 3);
pq.Enqueue("B", 1);
pq.Enqueue("C", 2);
pq.Enqueue("D", 3);
pq.Dequeue(); // returns "B"
pq.Dequeue(); // returns "C"
pq.Dequeue(); // either "A" or "D", stability is not guaranteed.
歸功于Patryk Golebiowski。
CollectionsMarshal.GetValueRef是一個新的 不安全 API,它可以更快地更新字典中的結構值。新API 旨在用于高性能場景,而不是用于一般用途。它返回ref結構值,然后可以使用典型技術對其進行更新。
以下示例演示了如何使用新API:
ref MyStruct value = CollectionsMarshal.GetValueRef(dictionary, key);
// Returns Unsafe.NullRef<TValue>() if it doesn't exist; check using Unsafe.IsNullRef(ref value)
if (!Unsafe.IsNullRef(ref value))
{
// Mutate in-place
value.MyInt++;
}
在此更改之前,更新struct字典值對于高性能場景可能會很昂貴,需要字典查找和復制到堆棧的struct. 然后在更改之后struct,它將再次分配給字典鍵,從而導致另一個查找和復制操作。這種改進將密鑰散列減少到1(從2)并刪除了所有結構復制操作。
歸功于本亞當斯。
添加了僅限日期和時間的結構,具有以下特征:
這種改進具有以下好處:
在所有平臺上支持 Windows 和 IANA 時區
這種改進具有以下好處:
改進的時區顯示名稱
Unix 上的時區顯示名稱已得到改進:
還進行了以下附加改進:
改進了對 Windows ACL 的支持
System.Threading.AccessControl現在包括對與Windows 訪問控制列表(ACL) 交互的改進支持。新的重載被添加到Mutex和Semaphore的OpenExisting和TryOpenExisting方法EventWaitHandle中。這些具有“安全權限”實例的重載允許打開使用特殊Windows 安全屬性創建的線程同步對象的現有實例。
此更新與.NET Framework 中可用的API 匹配并且具有相同的行為。
以下示例演示了如何使用這些新API。
對于Mutex:
var rights = MutexRights.FullControl;
string mutexName = "MyMutexName";
var security = new MutexSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
MutexAccessRule accessRule = new MutexAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);
// createdMutex, openedMutex1 and openedMutex2 point to the same mutex
Mutex createdMutex = MutexAcl.Create(initiallyOwned: true, mutexName, out bool createdNew, security);
Mutex openedMutex1 = MutexAcl.OpenExisting(mutexName, rights);
MutexAcl.TryOpenExisting(mutexName, rights, out Mutex openedMutex2);
為了Semaphore
var rights = SemaphoreRights.FullControl;
string semaphoreName = "MySemaphoreName";
var security = new SemaphoreSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
SemaphoreAccessRule accessRule = new SemaphoreAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);
// createdSemaphore, openedSemaphore1 and openedSemaphore2 point to the same semaphore
Semaphore createdSemaphore = SemaphoreAcl.Create(initialCount: 1, maximumCount: 3, semaphoreName, out bool createdNew, security);
Semaphore openedSemaphore1 = SemaphoreAcl.OpenExisting(semaphoreName, rights);
SemaphoreAcl.TryOpenExisting(semaphoreName, rights, out Semaphore openedSemaphore2);
為了EventWaitHandle
var rights = EventWaitHandleRights.FullControl;
string eventWaitHandleName = "MyEventWaitHandleName";
var security = new EventWaitHandleSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
EventWaitHandleAccessRule accessRule = new EventWaitHandleAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);
// createdHandle, openedHandle1 and openedHandle2 point to the same event wait handle
EventWaitHandle createdHandle = EventWaitHandleAcl.Create(initialState: true, EventResetMode.AutoReset, eventWaitHandleName, out bool createdNew, security);
EventWaitHandle openedHandle1 = EventWaitHandleAcl.OpenExisting(eventWaitHandleName, rights);
EventWaitHandleAcl.TryOpenExisting(eventWaitHandleName, rights, out EventWaitHandle openedHandle2);
HMAC 一次性方法
System.Security.CryptographyHMAC類現在具有允許一次性計算HMAC而無需分配的靜態方法。這些添加類似于在先前版本中添加的用于哈希生成的一次性方法。
該DependentHandle類型現在是公共的,具有以下 API 表面:
namespace System.Runtime
{
public struct DependentHandle : IDisposable
{
public DependentHandle(object? target, object? dependent);
public bool IsAllocated { get; }
public object? Target { get; set; }
public object? Dependent { get; set; }
public (object? Target, object? Dependent) TargetAndDependent { get; }
public void Dispose();
}
}
它可用于創建高級系統,例如復雜的緩存系統或ConditionalWeakTable<TKey, TValue>類型?的自定義版本。例如,它將被MVVM Toolkit中的WeakReferenceMessenger類型使用,以避免在廣播消息時分配內存。
可移植線程池
.NET 線程池已作為托管實現重新實現,現在用作.NET 6 中的默認線程池。我們進行此更改以使所有.NET 應用程序都可以訪問同一個線程池,而不管是否正在使用CoreCLR、Mono 或任何其他運行時。作為此更改的一部分,我們沒有觀察到或預期任何功能或性能影響。
該團隊在此版本中對.NET JIT 編譯器進行了許多改進,在每個預覽帖子中都有記錄。這些更改中的大多數都提高了性能。這里介紹了一些RyuJIT 的亮點。
動態 PGO
在.NET 6 中,我們啟用了兩種形式的PGO(配置文件引導優化):
動態PGO 已經在文章前面的性能部分中介紹過。我將提供一個重新上限。
動態PGO 使JIT 能夠在運行時收集有關實際用于特定應用程序運行的代碼路徑和類型的信息。然后,JIT 可以根據這些代碼路徑優化代碼,有時會顯著提高性能。我們在測試和生產中都看到了兩位數的健康改進。有一組經典的編譯器技術在沒有PGO 的情況下使用JIT 或提前編譯都無法實現。我們現在能夠應用這些技術。熱/冷分離是一種這樣的技術,而去虛擬化是另一種技術。
要啟用動態PGO,請在應用程序將運行的環境中進行設置DOTNET\_TieredPGO=1。
如性能部分所述,動態PGO 將TechEmpower JSON"MVC"套件每秒的請求數提高了26%(510K -> 640K)。這是一個驚人的改進,無需更改代碼。
我們的目標是在未來的.NET 版本中默認啟用動態PGO,希望在.NET 7 中啟用。我們強烈建議您在應用程序中嘗試動態PGO 并向我們提供反饋。
完整的 PGO
要充分利用Dynamic PGO,您可以設置兩個額外的環境變量:DOTNET\_TC\_QuickJitForLoops=1和DOTNET\_ReadyToRun=0。 這確保了盡可能多的方法參與分層編譯。我們將此變體稱為 Full PGO 。與動態PGO 相比,完整PGO 可以提供更大的穩態性能優勢,但啟動時間會更慢(因為必須在第0 層運行更多方法)。
您不希望將此選項用于短期運行的無服務器應用程序,但對于長期運行的應用程序可能有意義。
在未來的版本中,我們計劃精簡和簡化這些選項,以便您可以更簡單地獲得完整PGO 的好處并用于更廣泛的應用程序。
靜態 PGO
我們目前使用 靜態 PGO 來優化.NET 庫程序集,例如R2R(Ready To Run)附帶的程序集System.Private.CoreLib。
靜態PGO 的好處是,在使用crossgen 將程序集編譯為R2R 格式時會進行優化。這意味著有運行時的好處而沒有運行時成本。這是非常重要的,也是PGO 對C++ 很重要的原因,例如。
循環對齊
內存對齊是現代計算中各種操作的共同要求。在.NET 5 中,我們開始在 32 字節邊界對齊方法。在.NET 6 中,我們添加了一項執行自適應循環對齊的功能,該功能在具有循環的方法中添加NOP填充指令,以便循環代碼從mod(16) 或mod(32) 內存地址開始。這些更改改進并穩定了.NET 代碼的性能。
在下面的冒泡排序圖中,數據點1 表示我們開始在32 字節邊界對齊方法的點。數據點2 表示我們也開始對齊內部循環的點。如您所見,基準測試的性能和穩定性都有很大提高。
硬件加速結構
結構是CLR 類型系統的重要組成部分。近年來,它們經常被用作整個.NET 庫中的性能原語。最近的例子ValueTask是ValueTuple和Span<T>。記錄結構是一個新的例子。在.NET 5 和.NET 6 中,我們一直在提高結構的性能,部分原因是通過確保結構是局部變量、參數或方法的返回值時可以保存在超快速CPU 寄存器中)。這對于使用向量計算的API 特別有用。
穩定性能測量
團隊中有大量從未出現在博客上的工程系統工作。這對于您使用的任何硬件或軟件產品都是如此。JIT 團隊開展了一個項目來穩定性能測量,目標是增加我們內部性能實驗室自動化自動報告的回歸值。這個項目很有趣,因為需要進行深入調查和產品更改才能實現穩定性。它還展示了我們為保持和提高績效而衡量的規模。
此圖像演示了不穩定的性能測量,其中性能在連續運行中在慢速和快速之間波動。x 軸是測試日期,y 軸是測試時間,以納秒為單位。到圖表末尾(提交這些更改后),您可以看到測量值穩定,結果最好。這張圖片展示了一個單一的測試。還有更多測試在dotnet/runtime #43227中被證明具有類似的行為。
Crossgen2 是crossgen 工具的替代品。它旨在滿足兩個結果:
這種轉換有點類似于本機代碼csc.exe 到托管代碼Roslyn 編譯器。Crossgen2 是用C# 編寫的,但是它沒有像Roslyn 那樣公開一個花哨的API。
我們可能已經/已經為.NET 6 和7 計劃了六個項目,這些項目依賴于crossgen2。矢量指令默認提議是我們希望為.NET 6 但更可能是.NET 7 進行的crossgen2 功能和產品更改的一個很好的例子。版本氣泡是另一個很好的例子。
Crossgen2 支持跨操作系統和架構維度的交叉編譯(因此稱為"crossgen")。這意味著您將能夠使用單個構建機器為所有目標生成本機代碼,至少與準備運行的代碼相關。但是,運行和測試該代碼是另一回事,為此您需要合適的硬件和操作系統。
第一步是用crossgen2編譯平臺本身。我們使用.NET 6 完成了所有架構的任務。因此,我們能夠在此版本中淘汰舊的crossgen。請注意,crossgen2 僅適用于CoreCLR,而不適用于基于Mono 的應用程序(它們具有一組單獨的代碼生成工具)。
這個項目——至少一開始——并不以性能為導向。目標是啟用更好的架構來托管RyuJIT(或任何其他)編譯器以離線方式生成代碼(不需要或啟動運行時)。
你可能會說“嘿……如果是用C# 編寫的,難道你不需要啟動運行時來運行crossgen2 嗎?” 是的,但這不是本文中“離線”的含義。當crossgen2 運行時,我們不使用運行crossgen2 的運行時附帶的JIT 來生成準備運行(R2R) 代碼. 那是行不通的,至少對于我們的目標來說是行不通的。想象一下crossgen2 在x64 機器上運行,我們需要為Arm64 生成代碼。Crossgen2 將Arm64 RyuJIT(針對x64 編譯)加載為原生插件,然后使用它生成Arm64 R2R 代碼。機器指令只是保存到文件中的字節流。它也可以在相反的方向工作。在Arm64 上,crossgen2 可以使用編譯為Arm64 的x64 RyuJIT 生成x64 代碼。我們使用相同的方法來針對x64 機器上的x64 代碼。Crossgen2 會加載一個RyuJIT,它是為任何需要的配置而構建的。這可能看起來很復雜,但如果您想啟用無縫的交叉定位模型,它就是您需要的那種系統,而這正是我們想要的。
我們希望只在一個版本中使用術語“crossgen2”,之后它將替換現有的crossgen,然后我們將回到使用術語“crossgen”來表示“crossgen2”。
EventPipe 是我們用于在進程內或進程外輸出事件、性能數據和計數器的跨平臺機制。從.NET 6 開始,我們已將實現從C++ 移至C。通過此更改,Mono 也使用EventPipe。這意味著CoreCLR 和Mono 都使用相同的事件基礎設施,包括.NET 診斷CLI 工具。
這一變化還伴隨著CoreCLR 的小幅減?。?/span>
庫 | 大小之后 - 大小之前 | 差異 |
libcoreclr.so | 7037856 – 7049408 | -11552 |
我們還進行了一些更改,以提高 EventPipe 在負載下的吞吐量。在最初的幾個預覽版中,我們進行了一系列更改,從而使吞吐量提高了.NET 5 的2.06 倍:
對于這個基準,越高越好。.NET 6 是橙色線,.NET 5 是藍色線。
對.NET SDK 進行了以下改進。
.NET 6 SDK 可選工作負載的 CLI 安裝
.NET 6 引入了SDK 工作負載的概念。工作負載是可選組件,可以安裝在.NET SDK 之上以啟用各種場景。.NET 6 中的新工作負載是:.NET MAUI 和Blazor WebAssembly AOT 工作負載。我們可能會在.NET 7 中創建新的工作負載(可能來自現有的SDK)。工作負載的最大好處是減少大小和可選性。我們希望隨著時間的推移使SDK 變得更小,并且只安裝您需要的組件。這個模型對開發者機器有好處,對CI 來說甚至更好。
Visual Studio 用戶并不真正需要擔心工作負載。工作負載功能經過專門設計,以便像Visual Studio 這樣的安裝協調器可以為您安裝工作負載??梢酝ㄟ^CLI 直接管理工作負載。
工作負載功能公開了用于管理工作負載的多個動詞,包括以下幾個:
update動詞查詢更新nuget.org的工作負載清單、更新本地清單、下載已安裝工作負載的新版本,然后刪除所有舊版本的工作負載。這類似于apt update && apt upgrade -y(用于基于Debian 的Linux 發行版)。將工作負載視為SDK 的私有包管理器是合理的。它是私有的,因為它僅適用于SDK 組件。我們將來可能會重新考慮這一點。這些dotnet workload命令在給定SDK 的上下文中運行。假設您同時安裝了.NET 6 和.NET 7。工作負載命令將為每個SDK 提供不同的結果,因為工作負載將不同(至少相同工作負載的不同版本)。
請注意,將http://NuGet.org 中的工作負載復制到您的SDK 安裝中,因此如果SDK 安裝位置受到保護(即在管理員/根位置),dotnet workload install則需要運行提升或使用sudo。
內置 SDK 版本檢查
為了更容易跟蹤SDK 和運行時的新版本何時可用,我們向.NET 6 SDK 添加了一個新命令。
dotnet sdk check
它會告訴您是否有可用于您已安裝的任何.NET SDK、運行時或工作負載的更新版本。您可以在下圖中看到新體驗。
dotnet new
您現在可以在http://NuGet.org 中搜索帶有.dotnet new --search
模板安裝的其他改進包括支持切換以支持私有NuGet 源的授權憑據。--interactive
安裝CLI 模板后,您可以通過和檢查更新是否可用。--update-check--update-apply
NuGet 包驗證
包驗證工具使NuGet 庫開發人員能夠驗證他們的包是否一致且格式正確。
這包括:
該工具是SDK 的一部分。使用它的最簡單方法是在項目文件中設置一個新屬性。
<EnablePackageValidation> true </EnablePackageValidation>
更多 Roslyn 分析儀
在.NET 5 中,我們提供了大約250 個帶有.NET SDK 的分析器。其中許多已經存在,但作為NuGet 包在帶外發送。我們為 .NET 6 添加了更多分析器。
默認情況下,大多數新分析器都在信息級別啟用。您可以通過如下配置分析模式在警告級別啟用這些分析器:<AnalysisMode>All</AnalysisMode>
我們為.NET 6 發布了我們想要的一組分析器(加上一些附加功能),然后將它們中的大多數做成了可供抓取的。社區添加了幾個實現,包括這些。
感謝Meik Tranel和Newell Clark。
為 Platform Compatibility Analyzer 啟用自定義防護
CA1416 平臺兼容性分析器已經使用OperatingSystem和RuntimeInformation中的方法識別平臺防護,例如OperatingSystem.IsWindows和OperatingSystem.IsWindowsVersionAtLeast。但是,分析器無法識別任何其他保護可能性,例如緩存在字段或屬性中的平臺檢查結果,或者在輔助方法中定義了復雜的平臺檢查邏輯。
為了允許自定義守衛的可能性,我們添加了新屬性 SupportedOSPlatformGuard并UnsupportedOSPlatformGuard使用相應的平臺名稱和/或版本注釋自??定義守衛成員。此注釋被平臺兼容性分析器的流分析邏輯識別和尊重。
用法
[UnsupportedOSPlatformGuard("browser")] // The platform guard attribute
#if TARGET_BROWSER
internal bool IsSupported => false;
#else
internal bool IsSupported => true;
#endif
[UnsupportedOSPlatform("browser")]
void ApiNotSupportedOnBrowser() { }
void M1()
{
ApiNotSupportedOnBrowser(); // Warns: This call site is reachable on all platforms.'ApiNotSupportedOnBrowser()' is unsupported on: 'browser'
if (IsSupported)
{
ApiNotSupportedOnBrowser(); // Not warn
}
}
[SupportedOSPlatform("Windows")]
[SupportedOSPlatform("Linux")]
void ApiOnlyWorkOnWindowsLinux() { }
[SupportedOSPlatformGuard("Linux")]
[SupportedOSPlatformGuard("Windows")]
private readonly bool _isWindowOrLinux = OperatingSystem.IsLinux() || OperatingSystem.IsWindows();
void M2()
{
ApiOnlyWorkOnWindowsLinux(); // This call site is reachable on all platforms.'ApiOnlyWorkOnWindowsLinux()' is only supported on: 'Linux', 'Windows'.
if (_isWindowOrLinux)
{
ApiOnlyWorkOnWindowsLinux(); // Not warn
}
}
}
歡迎使用.NET 6。它是另一個巨大的.NET 版本,在性能、功能、可用性和安全性方面都有很多的改進。我們希望您能找到許多改進,最終使您在日常開發中更有效率和能力,并提高性能或降低生產中應用程序的成本。我們已經開始從那些已經開始使用.NET 6 的人那里聽到好消息。
在Microsoft,我們還處于.NET 6 部署的早期階段,一些關鍵應用程序已經投入生產,未來幾周和幾個月內還會有更多應用程序推出。
.NET 6 是我們最新的LTS 版本。我們鼓勵每個人都轉向它,特別是如果您使用的是.NET 5。我們期待它成為有史以來采用速度最快的.NET 版本。
此版本是至少1000 人(但可能更多)的結果。這包括來自Microsoft 的.NET 團隊以及社區中的更多人。我試圖在這篇文章中包含許多社區貢獻的功能。感謝您抽出寶貴時間創建這些內容并完成我們的流程。我希望這次經歷是一次美好的經歷,并且更多的人會做出貢獻。
這篇文章是許多有才華的人合作的結果。貢獻包括團隊在整個發布過程中提供的功能內容、為此最終帖子創建的重要新內容,以及使最終內容達到您應得的質量所需的大量技術和散文更正。很高興為您制作它和所有其他帖子。
感謝您成為.NET 開發人員。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。