==目標 ==?
處理大內(nèi)存的性能關(guān)鍵計算應(yīng)用程序工作集已經(jīng)運行在libhugetlbfs之上,然后依次運行hugetlbfs。透明的巨型頁面支持是另一種使用大頁為虛擬內(nèi)存提供大頁支持的方法, 該支持自動提升和降低頁面大小和沒有hugetlbfs的缺點。
目前它只適用于匿名內(nèi)存映射和tmpfs/shmem。但是將來它可以擴展到其他文件系統(tǒng)。實際上,已經(jīng)支持了只讀的文件映射。
應(yīng)用程序運行更快的原因有兩個的因素。第一個因素幾乎完全無關(guān)緊要,事實并非如此,這很重要,因為它也有缺點在頁錯誤中需要更大的清除頁拷貝有潛在的負面影響。第一個因素是采取每個2M的虛擬區(qū)域都有一個頁面錯誤(將內(nèi)核的進入/退出頻率減少512倍)。這的生命周期中,一個內(nèi)存映射只有第一次訪問內(nèi)存。第二個更持久,也更重要因子將會影響應(yīng)用程序的運行時整個內(nèi)存的所有后續(xù)訪問。第二個因素有兩個組件: 1)TLB miss將運行更快(特別是使用嵌套分頁的虛擬化,但幾乎總是在沒有虛擬化的裸系統(tǒng)上。2)單個TLB條目將是映射更大數(shù)量的虛擬內(nèi)存,從而減少TLB miss次數(shù)。使用虛擬化和嵌套分頁只有KVM和Linux客戶端同時支持映射更大的TLB正在使用大頁面,但顯著的速度已經(jīng)發(fā)生了,如果其中一個使用大頁面只是因為TLB miss會跑得更快。
== 設(shè)計 ==
“優(yōu)雅回退”:內(nèi)存組件沒有透明的巨型頁面知識可以回退到將巨型的PMD映射分解成ptes表,如果有必要,分裂一個透明的大頁面。因此這些組件可以繼續(xù)在常規(guī)頁面或常規(guī)pte映射上工作。
?如果由于內(nèi)存碎片而導致大頁面分配失敗,常規(guī)頁面應(yīng)該優(yōu)雅地分配和混合在相同的vma中,沒有任何故障或重大延遲,沒有用戶感知。
如果某個任務(wù)退出了,并且出現(xiàn)了更多可用的大頁面(要么立即在buddy中或者通過VM),由常規(guī)頁面支持的guest物理內(nèi)存應(yīng)該重新自動的安放在大頁面上(通過khugepaged線程)。
它不需要內(nèi)存預(yù)留,并且盡可能地使用大頁(這里唯一可能的預(yù)留是kernelcore=,以避免不可移動的頁面碎片化所有內(nèi)存,但這樣的調(diào)整不是針對透明大頁支持的,它是通用的適用于內(nèi)核中所有動態(tài)高階分配的特性)
?透明大頁支持最大限度地利用空閑內(nèi)存,如果與hugetlbfs的保留方法相比,允許所有未使用的內(nèi)存用作緩存或其他可移動(甚至不可移動的對象)。它不需要預(yù)留來防止從用戶空間發(fā)現(xiàn)大頁面分配失敗。它允許分頁和所有其他高級vm功能在大頁上。應(yīng)用程序不需要修改就可以利用它。
然而,應(yīng)用程序可以進一步優(yōu)化以利用這個功能,就像他們之前優(yōu)化過避免每個malloc(4k)都需要大量的mmap系統(tǒng)調(diào)用。優(yōu)化用戶空間到目前為止不是強制性的,khugepaged已經(jīng)可以照顧長生命周期的頁面分配,即使對于處理大量內(nèi)存的不知道大頁的應(yīng)用程序也是如此。
在某些情況下,當啟用大頁面時,系統(tǒng)范圍內(nèi),應(yīng)用程序可能最終會分配更多的內(nèi)存資源。一個應(yīng)用程序可以映射一個大的區(qū)域,但只觸及其中1字節(jié),在這種情況下,一個2M的頁面可能被分配而不是分配一個4k頁面是沒有好處的。這就是為什么可以在系統(tǒng)范圍內(nèi)禁用大頁面,并且只在內(nèi)部使用它們MADV_HUGEPAGE的madvise的區(qū)域。
嵌入式系統(tǒng)應(yīng)該只在madvise區(qū)域內(nèi)啟用大頁面為了消除浪費寶貴內(nèi)存字節(jié)的風險,并且只會跑得更快。
應(yīng)用程序可以從大頁中獲得很多好處,而不可以冒著丟失內(nèi)存的風險使用大頁,應(yīng)該使用madvise(MADV_HUGEPAGE)在他們關(guān)鍵映射區(qū)域。
== sysfs ==
透明大頁支持匿名內(nèi)存能被完全的禁用(主要是為了調(diào)試)或僅在MADV_HUGEPAGE區(qū)域內(nèi)啟用(避免占用更多內(nèi)存資源的風險)或者系統(tǒng)范圍內(nèi)啟用。這可以通過以下方式實現(xiàn):
?
echo never >/sys/kernel/mm/transparent_hugepage/enabled echo always >/sys/kernel/mm/transparent_hugepage/enabled echo?madvise?>/sys/kernel/mm/transparent_hugepage/enabled?
?
還可以限制VM中的碎片整理工作,以生成匿名的巨型頁面,以防它們不能立即自由地使用madvise區(qū)域,或者永遠不要嘗試對內(nèi)存進行碎片整理,而只是回退到常規(guī)頁面,除非巨型頁面立即可用。顯然,如果我們花費CPU時間對內(nèi)存進行碎片整理,那么我們將期望獲得更多的好處,因為我們稍后使用了大頁面而不是普通頁面。這不是總能保證的,更可能的情況是分配給一個MADV_HUGEPAGE區(qū)域。
?
echo always >/sys/kernel/mm/transparent_hugepage/defrag echo defer >/sys/kernel/mm/transparent_hugepage/defrag echo defer+madvise >/sys/kernel/mm/transparent_hugepage/defrag echo madvise >/sys/kernel/mm/transparent_hugepage/defrag echo never >/sys/kernel/mm/transparent_hugepage/defrag
?
“always”意味著請求THP的應(yīng)用程序?qū)⒃诜峙涫r暫停,并直接回收頁面和規(guī)整內(nèi)存,以便立即分配THP。對于那些從THP使用中受益頗多并愿意延遲虛擬機開始使用它們的虛擬機來說,這可能是可取的。
“defer”意味著應(yīng)用程序?qū)⒃诤笈_喚醒kswapd來回收頁面,并喚醒kcompactd來規(guī)整內(nèi)存,以便在不久的將來THP可用。khugepage負責隨后安裝THP頁面。
"defer+madvise"只對已經(jīng)使用madvise(MADV_HUGEPAGE)的區(qū)域,后臺喚醒kswapd以回收頁面,并喚醒kcompactd以規(guī)整內(nèi)存,以便THP在不久的將來可用。
"madvise"將進入直接回收,像"always",但只對madvise(MADV_HUGEPAGE)的區(qū)域。這是默認行為。
“never”應(yīng)該是不言自明的,它不采取任何措施。
默認情況下,內(nèi)核嘗試在讀取頁面錯誤時使用巨型零頁來進行匿名映射。可以通過寫入0來禁用巨型0頁,也可以通過寫入1來啟用巨型0頁:
?
echo 0 >/sys/kernel/mm/transparent_hugepage/use_zero_page echo 1 >/sys/kernel/mm/transparent_hugepage/use_zero_page
?
一些用戶空間(比如一個測試程序,或者一個優(yōu)化的內(nèi)存分配庫)可能想知道一個透明大頁的大小(以字節(jié)為單位):
?
cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size
?
?當transparent_hugepage/enabled設(shè)置為“always”或“madvise”時,khugepaged將自動啟動,如果設(shè)置為“never”,它將自動關(guān)閉。
?khugepaged的運行頻率通常較低,因此,雖然人們可能不希望在缺頁異常期間同步調(diào)用碎片整理算法,但至少在khugepaged中調(diào)用碎片整理是值得的。但是,也可以通過寫0來禁用khugepaged中的碎片整理,或者通過寫1來啟用khugepaged中的碎片整理:
?
echo 0 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag echo 1 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag
?
你也可以控制khugepaged每次通過時應(yīng)該掃描多少頁面:
?
/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
?
以及每次通過之間在khugepaged中等待毫秒數(shù)(你可以設(shè)置為0來運行khugepaged,在一個核的100%利用率):
?
/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
?
以及在khugepage中等待多少毫秒,如果有一個巨大的頁面分配失敗,以阻止下一次分配嘗試。
?
/sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs
?
?khugepaged的進度可以從坍縮的頁面數(shù)中看到:
?
/sys/kernel/mm/transparent_hugepage/khugepaged/pages_collapsed
?
每次通過:
?
/sys/kernel/mm/transparent_hugepage/khugepaged/full_scans
?
max_ptes_none指定有多少額外的小頁面(即尚未映射的)可以在踏縮一組小頁到大頁中被分配(查詢到相應(yīng)的頁表項為空)。
?
/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
?
?較高的值會導致程序使用額外的內(nèi)存。數(shù)值越低,獲得的thp性能越低。max_ptes_none值只會浪費很少的cpu時間,你可以忽略它。
max_ptes_swap指定當將一組頁面坍縮(collapse)成一個透明的大頁面時,可以從交換區(qū)換入多少頁面(查詢到相應(yīng)的頁表項為換出頁標識符)。。
?
/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_swap
?
較高的值會導致過多的交換IO并浪費內(nèi)存。較低的值可以防止thp被坍縮,從而導致更少的頁面坍縮進thp,內(nèi)存訪問性能較低。
== 啟動參數(shù) ==?
你可以更改透明大頁sysfs啟動時的默認值,通過傳遞參數(shù)"transparent_hugepage=always" 或"transparent_hugepage=madvise" 或 "transparent_hugepage=never"到內(nèi)核命令行。
?== tmpfs/shmem 中的大頁面 ==
您可以使用掛載選項控制tmpfs中的大頁分配策略"huge="。它可以有以下值:
"always": 每次需要新頁面時,嘗試分配大頁面;
"never": 不要分配大頁面;
"within_size": 只有它將完全在i_size內(nèi)時才分配大頁。也尊重fadvise()/madvise()提示;
"advise":? 只有在fadvise()/madvise()請求時才分配大頁面;
默認策略為“never”。
“mount -o remount,huge= /mountpoint”在掛載后工作良好:重新掛載huge=never根本不會分解大頁面,只是停止更多的分配。
還有一個sysfs接口可以控制內(nèi)部shmem掛載的大頁分配策略:
?
/sys/kernel/mm/transparent_hugepage/shmem_enabled。
?
掛載用于SysV SHM, memfds,共享匿名映射(/dev/zero或MAP_ANONYMOUS)GPU驅(qū)動的DRM對象,Ashmem。
除了上面列出的策略之外,shmem_enabled還允許另外兩個值:
"deny":?用于在緊急情況下使用,以強制關(guān)閉所有掛載的大頁選項;
"force": 為所有人強制提供大頁的選項——這對測試非常有用; ?
==需要重新啟動應(yīng)用程序==
transparent_hugepage/enabled值和tmpfs掛載選項只影響未來的行為。因此,為了使它們有效,您需要重新啟動任何可能使用大頁面的應(yīng)用程序。這也適用于在khugepaged中注冊的區(qū)域。
==監(jiān)控使用情況==
?當前使用的匿名透明大頁面的數(shù)量系統(tǒng)可以通過讀取/proc/meminfo中的AnonHugePages字段來訪問。為了識別哪些應(yīng)用程序正在使用匿名透明的大頁面,讀取/proc/PID/smaps并統(tǒng)計為每個映射的AnonHugePages字段是必要的。
?映射到用戶空間的文件透明大頁面數(shù)量可用通過讀取/proc/meminfo中的ShmemPmdMapped和ShmemHugePages字段。為了確定哪些應(yīng)用程序正在映射文件透明的巨大頁面,它讀取/proc/PID/smaps并統(tǒng)計為每個映射FileHugeMapped字段是必要的。
注意,讀取smaps文件時昂貴的,且經(jīng)常會產(chǎn)生開銷。
?在/proc/vmstat中有許多計數(shù)器可以用于監(jiān)視系統(tǒng)提供大頁面的成功程度。
thp_fault_alloc : 每當處理缺頁異常時,一個大頁面被成功分配,thp_fault_alloc就會增加。這適用于第一次出現(xiàn)缺頁異常和COW錯誤。
thp_collapse_alloc:當它發(fā)現(xiàn)一個范圍的頁面坍縮成一個大頁,并有成功分配一個新的巨大頁來存儲數(shù)據(jù),thp_collapse_alloc會被khugepaged增加。
thp_fault_fallback:?如果缺頁異常失敗的分配一個大頁,則thp_fault_fallback被增加,而回退使用小頁面。
?thp_collapse_alloc_failed:?當它發(fā)現(xiàn)一個范圍的頁面應(yīng)該被坍縮成一個大頁,但是分配大頁失敗,thp_collapse_alloc_failed會被khugepaged增加。
?thp_file_alloc:?在文件大頁成功分配時遞增。
thp_file_mapped:?每映射到一個文件大頁到用戶地址空間,thp_file_mapped就增加一次。
thp_split_page:在每次將一個巨大的頁面分裂為普通頁時遞增。發(fā)生這種情況的原因有很多,但都很常見原因是一個巨大的頁面是舊的,正在被回收。這個操作意味著分裂頁面映射的所有PMD。
thp_split_page_failed:如果內(nèi)核無法分裂大頁,則增加thp_split_page_failed計數(shù)。如果頁面被人pin住了,就會發(fā)生這種情況。
thp_deferred_split_page:當大頁被放到分裂隊列時,thp_deferred_split_page計數(shù)被增加。當一個巨大的頁面部分被unmap且分裂它將釋放一些內(nèi)存就會發(fā)生這種情況。分裂隊列上的頁將在內(nèi)存壓力下分裂。
thp_split_pmd:?每當pmd分裂成pte表時,thp_split_pmd就會遞增。例如,當應(yīng)用程序調(diào)用mprotect()或unmap()在大頁面的一部分。它不會分割大頁面,只是頁表條目。
?thp_zero_page_alloc:?thp_zero_page_alloc在每出現(xiàn)一個巨型零頁被成功地分配時遞增。它包括分配,放棄了與其他分配的競爭。注意,這不算每次巨型零頁的映射,只有它的分配。
thp_zero_page_alloc_failed:?如果內(nèi)核分配巨型零頁失敗并回退到使用小頁,則thp_zero_page_alloc_failed會增加。
?隨著系統(tǒng)老化,分配大頁的開銷可能會很大,因為系統(tǒng)會使用內(nèi)存規(guī)整在內(nèi)存周圍來復制數(shù)據(jù),以釋放大頁供使用。在/proc/vmstat中有一些計數(shù)器可以幫助監(jiān)視這種開銷。
compact_stall:?每當進程停滯去允許內(nèi)存規(guī)整時,compact_stall就會增加,以便一個巨大的頁面被釋放供使用。
compact_success:?如果系統(tǒng)規(guī)整內(nèi)存和釋放一個大頁面供使用,則compact_success會增加(成功規(guī)整的次數(shù))。
?compact_fail:?如果系統(tǒng)試圖規(guī)整內(nèi)存但是失敗了,則compact_fail會增加(失敗規(guī)整的次數(shù))。
compact_pages_moved:?每次移動頁面時,compact_pages_moved會增加。如果這個值是迅速增加的,說明該系統(tǒng)就是復制大量的數(shù)據(jù)來滿足大頁面分配。復制的成本可能超過任何減少TLB misse的節(jié)省。
compact_pagemigrate_failed:?在底層機制遞增移動頁面失敗,compact_pagemigrate_failed會增加(規(guī)整時,遷移頁面失敗次數(shù)) 。
compact_blocks_moved:?每次內(nèi)存規(guī)整檢查時一個大頁面對齊的頁面范圍,compact_blocks_moved會增加。
可以使用函數(shù)跟蹤器來記錄在__alloc_pages_nodemask中花費了多長時間,并使用mm_page_alloc跟蹤點來確定哪些分配用于巨大的頁面。
== get_user_pages and follow_page ==
get_user_pages和follow_page如果在一個巨型的頁面上運行,將返回往常一樣的頭頁或尾頁(就像他們在hugetlbfs上做的一樣)。大多數(shù)gup用戶只關(guān)心實際的物理屬性頁的地址和它的臨時固定在I/O之后釋放是完整的,所以他們不會注意到頁面是巨型的。但如果有任何驅(qū)動程序會在尾部的頁面結(jié)構(gòu)上損壞 page(用于檢查page->mapping或其他相關(guān)的位對于頭頁而不是尾頁),應(yīng)該更新為跳轉(zhuǎn)改為檢查頭頁。在任何頭/尾頁上引用都可以防止頁面被任何人分裂。
注意:這些不是GUP API的新約束,它們與hugetlbfs上的約束相同,所以任何能夠在hugetlbfs上處理GUP的驅(qū)動程序也可以很好地處理透明的大頁面支持映射。
如果您不能處理由follow_page返回的復合頁面,那么可以將FOLL_SPLIT位指定為follow_page的參數(shù),這樣它將在返回大頁面之前分裂它們。例如,遷移將FOLL_SPLIT作為參數(shù)傳遞給follow_page,因為它不知道巨型頁面,事實上它根本不能在hugetlbfs上工作(但由于FOLL_SPLIT,它在透明的巨型頁面上工作得很好)。遷移根本無法處理返回的大頁面(因為它不僅檢查頁面的PFN并在復制期間pin住它,而且?guī)в谐R?guī)的pte/pmd映射)。
==優(yōu)化應(yīng)用程序==
為了保證內(nèi)核將立即在任何內(nèi)存區(qū)域映射2M頁,mmap區(qū)域必須自然對齊。posix_memalign()可以提供這種保證。
== Hugetlbfs ==
您可以在內(nèi)核中使用hugetlbfs,并且始終很好地啟用了透明的超大頁支持。hugetlbfs中除了整體碎片更少之外,沒有什么不同。所有屬于hugetlbfs的常見特性都被保留且不受影響。libhugetlbfs也會像往常一樣正常工作。
==優(yōu)雅回退==
代碼遍歷頁表但不能感知巨型的pmds,可以簡單地調(diào)用split_huge_pmd(vma, pmd, addr),其中pmd是pmd_offset返回的那個。通過查詢“pmd_offset”并在pmd_offset返回pmd后丟失的地方添加split_huge_pmd,使代碼透明地感知大頁是很簡單的。多虧了優(yōu)雅的回退設(shè)計,只需一行代碼的更改,就可以避免編寫數(shù)百行(如果不是數(shù)千行的話)的復雜代碼,從而使代碼具有超大頁面的感知能力。
?如果您沒有遍歷頁表,但是遇到了一個物理的大頁,但是您不能在代碼中原生地處理它,您可以通過調(diào)用split_huge_page(page)來分裂它。這就是Linux VM在嘗試切換大頁面之前所做的。如果頁面被pin住,那么split_huge_page()可能會失敗,您必須正確處理這個問題。
?讓mremap.c透明感知hugepage的例子,只需要一行代碼的改變:
?
diff --git a/mm/mremap.c b/mm/mremap.c --- a/mm/mremap.c +++ b/mm/mremap.c @@ -41,6 +41,7 @@ static pmd_t *get_old_pmd(struct mm_stru return NULL; pmd = pmd_offset(pud, addr); + split_huge_pmd(vma, pmd, addr); if (pmd_none_or_clear_bad(pmd)) return NULL;
?
== 鎖定大頁面感知代碼 ==
我們希望盡可能多的代碼能夠感知大頁,因為調(diào)用 split_huge_page()或split_huge_pmd()是有代價的。
要使頁表遍歷感知巨型pmd,您所需要做的就是調(diào)用pmd_trans_huge()在由pmd_offset返回的PMD上。你必須持有mmap_sem處于讀(或?qū)?模式,以確保不能出現(xiàn)巨型PMD由khugepaged創(chuàng)建 (khugepaged坍縮巨型頁collapse_huge_page除anon_vma鎖外,還以寫模式持有mmap_sem)。如果pmd_trans_huge返回false,您只需返回到舊代碼路徑。如果pmd_trans_huge返回true,則必須持有頁表鎖(pmd_lock()),然后重新運行pmd_trans_huge。持有頁表鎖將防止巨型的PMD被轉(zhuǎn)換成一個常規(guī)的PMD(split_huge_pmd可以與頁表遍歷并行)。如果第二個pmd_trans_huge返回false,則應(yīng)該釋放頁表鎖并回退到之前的舊代碼中。否則,您可以繼續(xù)處理巨型的pmd和hugepage本身。一旦完成,您可以釋放頁表鎖。
?== 引用計數(shù)和透明大頁 ==
THP上的引用計數(shù)和其他復合頁的引用計數(shù)基本一致:
?get_page()/put_page() and GUP 在首頁的->_refcount中操作。
尾頁的->_refcoun總是0:get_page_unless_zero()從來不會在尾頁上成功。
map/unmap具有帶有PTE條目的頁面,增加/減小復合頁相關(guān)子頁上的->_mapcount。
map/unmap 整個復合頁的被記賬在compound_mapcount(存儲在第一個尾頁)。對于文件巨型頁,我們也增加所有子頁面的->_mapcount,以便無競爭檢測子頁面的最后一次unmap。
?PageDoubleMap()表示頁面可能映射了pte。
對于匿名頁面,PageDoubleMap()還表示->_mapcount在所有子頁面中被抵消了一個。此附加引用是必需的,當子頁面同時被映射到PMDs和 PTEs時,獲得對其子頁面unmap的無競爭檢測。
這是降低每個子頁面的mapcount跟蹤開銷所需的優(yōu)化。另一種方法是在整個復合頁面的每個map/unmap上的所有子頁面中添加 ->_mapcount。
?對于匿名頁面,當頁面的PMD被分裂時,但仍有PMD映射,我們設(shè)置PG_double_map。額外的引用去掉最后一個compound_mapcount。
文件頁面在帶有PTE和的頁面的第一個映射上設(shè)置PG_double_map ,當頁面從頁面緩存中被驅(qū)逐時,該頁面就會消失。
?split_huge_page內(nèi)部必須在從頭頁到尾頁分配refcount,然后清除頁面結(jié)構(gòu)中所有的PG_head/尾位。它可以很容易地實現(xiàn)頁表條目的引用計數(shù)。但我們沒有足夠的信息來分發(fā)額外的pins(即get_user_pages)。split_huge_page()請求去分裂pin住的大頁面是失敗的: 它期望頁面計數(shù)等于所有子頁面的mapcount之和加上1 (split_huge_page調(diào)用者必須有頭頁引用)。
split_huge_page使用遷移條目來穩(wěn)定匿名頁面的page->_refcount和page->_mapcount。文件頁面被取消映射。
我們和物理內(nèi)存掃描器(頁面回收的掃描器)競爭也是安全的:掃描器來獲取對頁面的引用唯一合法的方式是get_page_unless_zero()。
在atomic_add()之前,所有尾頁的->_refcount都為0。這可以防止掃描器獲取到尾頁的引用。在atomic_add()之后,我們不關(guān)心->_refcount值。我們已經(jīng)從頭頁上知道有多少引用是取消記賬的。
對于頭頁,get_page_unless_zero()會成功,我們不介意。它是明確拆分后引用應(yīng)該去哪里:它將停留在首頁。
注意split_huge_pmd()對refcount沒有任何限制:PMD可以在任何點被拆分并且永不失敗。
?== 部分 unmap and deferred_split_huge_page() ==
解除THP部分映射(使用munmap()或其他方式)不會立即釋放內(nèi)存。相反,我們在page_remove_rmap()中檢測到THP的一個子頁面沒有被使用,并在內(nèi)存壓力時,將THP排隊以進行拆分。分裂將釋放未使用的子頁面。
由于將上下文鎖住在我們可以檢測到部分unmap的地方,所以不能立即拆分頁面。這也可能會適得其反,因為在許多情況下,如果THP跨越VMA邊界,在exit(2)期間會發(fā)生部分unmap。
用于對頁面進行排隊以進行拆分。當我們通過shrinker收縮器接口獲得內(nèi)存壓力時,分裂本身就會發(fā)生。
評論