成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

V8 引擎:基于類型推測的性能優化原理

開發 前端
本文的會介紹一些關于V8內基于推測的優化的技術,以此來告訴大家,為什么需要TypeScript。

介紹

本文的會介紹一些關于V8內基于推測的優化的技術,以此來告訴大家,為什么需要TypeScript。

我們將以一段函數的執行未展開,從函數執行的角度來看看,一段代碼如何被執行,優化,再最后,你會了解,為什么TypeScript更好。

看完本文后,你不需要記住文章中出現的繁雜的指令和代碼,只需要在你的腦海中存在一個印象,避免寫出糟糕的代碼,以及,盡量使用TypeScript。

如何執行代碼?

作為介紹的第一部分,我們會用一段簡短的篇幅帶大家看看,你的代碼如何被執行

圖片

當然,如果用簡單的流程圖表示,你可以把上面的過程理解為這樣一個線性的執行過程,當然可能并不嚴謹,稍后我們會繼續介紹。

圖片

下面讓我們從一段具體的代碼來看一下這個過程。

一段簡單的代碼?

function add(x, y) {
return x + y;
}
console.log(add(1, 2))

如果你在chrome的DevTools console中運行這段代碼,你可以看到預期的輸出值3。

圖片

根據上面的流程圖,這段代碼被執行的第一步,是被解析器解析為AST,這一步我們用d8 shell 的Debug版本中使用 –print-ast 命令來查看V8內部生成的AST。

$ out/Debug/d8 --print-ast add.js
-
-- AST ---
FUNC at 12
. KIND 0
. SUSPEND COUNT 0
. NAME "add"
. PARAMS
. . VAR (0x7fbd5e818210) (mode = VAR) "x"
. . VAR (0x7fbd5e818240) (mode = VAR) "y"
. RETURN at 23
. . ADD at 32
. . . VAR PROXY parameter[0] (0x7fbd5e818210) (mode = VAR) "x"
. . . VAR PROXY parameter[1] (0x7fbd5e818240) (mode = VAR) "y

很多人可能或多或少接觸過AST的概念,這里不多贅述,只是用一張簡單的圖表示下上面的過程。

圖片

最開始,函數字面量add被解析為樹形表示,其中一個子樹用于參數聲明,另外一個子樹用于實際的的函數體。在解析階段,不可能知道程序中名稱和變量的綁定關系,這主要是因為“有趣的變量聲明提升規則”以及JavaScript中的eval,此外還有其他原因。

一旦我們構建完成了AST,它便包含了從中生成可執行字節碼的所有必要信息。AST隨后被傳遞給BytecodeGenerator ,BytecodeGenerator 是屬于Ignition 的一部分,它以函數為單位生成字節碼(_其他引擎并不一定以函數為單位生成的_)。你也可以在d8中使用命令–print-bytecode來查看V8生成的字節碼(或者用node端)

$ out/Debug/d8 --print-bytecode add.js
[
generated bytecode for function: add]
Parameter count 3
Frame size 0
12 E> 0x37738712a02a @ 0 : 94 StackCheck
23 S> 0x37738712a02b @ 1 : 1d 02 Ldar a1
32 E> 0x37738712a02d @ 3 : 29 03 00 Add a0, [0]
36 S> 0x37738712a030 @ 6 : 98 Return
Constant pool (size = 0)
Handler Table (size = 16)

上面過程中為函數add生成了一個新的字節碼對象,它接受三個參數,一個內部的this引用,以及兩個顯式形參x和y。該函數不需要任何的局部變量(所以棧幀大小為0),并且包含下面這四個字節碼指令組成的序列

StackCheck
Ldar a1
Add a0, [0]
Return

為了解釋這段字節碼,我們首先需要從較高的層面來認知解釋器如何工作。V8的解釋器是基于寄存器架構(register machine)的(相對的是基于棧架構,也是早期V8版本中使用的 FullCodegen 編譯器)。Ignition 會把指令序列都保存在解釋器自身的(虛擬)寄存器中,這些寄存器部分被映射到實際CPU的寄存器中,而另外一部分會用實際機器的棧內存來模擬。

圖片

有兩個特殊寄存器a0和a1對應著函數在機器棧(即內存棧)上的形式參數(在函數add這個例子中,有兩個形參)。形參是在源代碼中聲名的參數,它可能與在運行時傳遞給函數的實際參數數量不同。每個字節碼指令執行得到的最終值通常被保存在一個稱作累加器(accumulator)的特殊寄存器中。堆棧指針(stack pointer )指向當前的棧幀或者說激活記錄,程序計數器( program counter)指向指令字節碼中當前正在執行的指令。下面我們看看這個例子中每條字節碼指令都做了什么。

  • StackCheck 會將堆棧指針與一些已知的上限比較(實際上在V8中應該稱作下限,因為棧是從高地址到低地址向下生長的)。如果棧的增長超過了某個閾值,就會放棄函數的執行,同時拋出一個 RangeError 來告訴我們棧溢出了。
  • Ldar a1將寄存器a1的值加載到累加器寄存器中(Ladr 表示 LoaD Accumulator Register)
  • Add a0, [0] 讀取寄存器a0里的值,并把它加到累加器的值上。結果被再次放到累加器中。

為什么這條指令是這樣的?以一條JS 語句為例

var dest = src1 + src2 // op dest, src1,src2
var dest += src; //op dest, src
+src; // op src

分別表示三地址指令,二地址指令,一地址指令,我在后面分別標注了轉換后的機器指令。三地址和二地址指令都指定了運算后儲存結果的位置。

但在一地址指令中,沒有指定目標源。實際上,它會被默認存在一個累加器”(accumulator)的專用寄存器,保存計算結果。

其中Add運算符的[0]操作數指向一個「反饋向量槽( feedback vector slot)」,它是解釋器用來儲存有關函數執行期間看到的值的分析信息。在后面講解TurboFan 如何優化函數的時候會再次回到這。

  • Return 結束當前函數的執行,并把控制權交給調用者。返回值是累加器中的當前值。

當最終生成了上面這段字節碼后,會被送入的VM ,一般會由解釋器進行執行,這種執行方式是最原始也是效率最低的。我們可以在下一部分了解到,這種原始的執行會經歷什么。

關于字節碼的解釋,這里不會做過多的贅述,如果你感興趣,可以擴展閱讀 「Understanding V8’s Bytecode」 (https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775) 一文,這篇字節碼對V8的字節碼的工作原理提供了一些深入的了解。

為什么需要優化?

現在,我相信你已經對V8如何執行一段代碼有了一個簡單的認識。在正式進入我們的主題之前,還需要解釋一個很關鍵的問題,為什么我們需要優化。為了回答這個問題,我們需要先看下下規范

關于規范的更多了解,可以去這里查找 https://tc39.es/ecma262/#sec-toprimitive

圖片

圖片

我們再以看最常見的 ToPrimitive 為例,需要經過非常繁瑣的求值過程,而這些過程都是為了解決操作類型的動態性

圖片

在JavaScript中的“+”運算符已經是一個相當復雜的操作了,在最終執行一個數值相加之前必須進行大量的檢查

而如果引擎要想讓這些步驟是能夠在幾個機器指令內完成以達到峰值性能(與C++相媲美),這里有一個關鍵能力—-推測優化,通過假設可能的輸入。例如,當我們知道表達式x+y中,x和y都是數字,那么我們就不需要處理任何一個是字符串或者其他更糟糕的情況—-操作數是任意類型的JavaScript對象,也就不需要對所有參數調用一遍 ToPrimitive 了。

換句話說,如果我們能夠確定x,y 都是數字類型,我們自然就很容易對這個函數執行進行優化,消除冗余的IR指令。

「而從執行的角度來說,動態類型性能瓶頸很大程度是因為它的動態的類型系統,與靜態類型的語言相比,JavaScript 程序需要額外的操作來處理類型的動態性,所以執行效率比較低。」

那么如何確認x,y都是數字,我們又如何優化呢?

基于推測的優化?

因為 JavaScript 動態語言的特性,我們通常直到運行時才知道值的確切類型,僅僅觀察源代碼,往往不可能知道某個操作的可能輸入值。所以這就是為什么我們需要推測,根據之前運行收集到的值的反饋,然后假設將來總會看到類似的值。這種方法聽起來可能作用相當有限,但它已被證明適用于JavaScript這樣的動態語言。

你可能會聯想到CPU的分支預測能力,如果是這樣,那么恭喜你,你并沒有想錯。

我們再回到這段代碼

function add(x, y) {
return x + y;
}

你可能已經想到了,作為一個動態類型的語言,推測的第一步,就是要收集到足夠多的信息,來預測 ??add?? 在今后的執行中會遇到的類型。

所以,首先向你介紹反饋向量(Feedback Vector),它是我們執行預測最核心的成員之一:負責儲存我們收集到的信息。

反饋向量(Feedback Vector)

當一段代碼被初次執行時,它所執行的往往是解釋器產生的字節碼。當這段字節碼每次的執行后,都會會產生一些反饋信息,這些反饋信息會被儲存在「反饋向量」(過去叫類型反饋向量) 中,這個特殊的數據結構會被鏈接在閉包上。如果從對象結構的角度來看,反饋向量和其他相關的內容會是這樣。

圖片

其中 SharedFunctionInfo,它包含了函數的一般信息,比如源位置,字節碼,嚴格或一般模式。除此之外,還有一個指向上下文的指針,其中包含自由變量的值以及對全局對象的訪問。

關于自由變量和約束變量的概念, 閉包 (計算機科學)

反饋向量的大致結構如下,slot是一個槽,表示向量表里面的一項,包含了操作類型和傳入的值類型,

IC Slot

IC Type

Value

1

Call

UNINIT

2

BinaryOp

SignedSmall

比如,第二個是一個 BinaryOp 槽,二元操作符類似“+,-”等能夠記錄迄今為止看到的輸入和輸出的反饋。先不用糾結它的含義,后面我們會具體介紹。

如果你想查看你的函數所對應的反饋向量,可以在你的代碼中加上專門的內部函數 ??%DebugPrint???  ,并且在d8中加上命令 ??–allow-natives-syntax?? 來檢查特定閉包的反饋向量的內容。

源代碼:

function add(x, y) {
return x + y;
}
console.log(add(1, 2));
%DebugPrint(add);

在d8 使用這個命令 –allow-natives-syntax 運行,我們看到 :

$ out/Debug/d8 --allow-natives-syntax add.js
DebugPrint: 0xb5101ea9d89: [Function] in OldSpace
- feedback vector: 0xb5101eaa091: [FeedbackVector] in OldSpace
- length: 1
SharedFunctionInfo: 0xb5101ea99c9 <SharedFunctionInfo add>
Optimized Code: 0
Invocation Count: 1
Profiler Ticks: 0
Slot #0 BinaryOp BinaryOp:SignedSmall

我們看到調用次數(Invocation Count)是1,因為我們只調用了一次函數add。此時還沒有優化代碼(根據Optimized Code的值為0)。反饋向量的長度為1,說明里面只有一個槽,就是我們上面說到的二元操作符槽(BinaryOp Slot),當前反饋為 SignedSmall。

這個反饋SignedSmall代表什么?這表明指令Add只看到了SignedSmall類型的輸入,并且直到現在也只產生了SignedSmall類型的輸出。

但是什么是SignedSmall類型?JavaScript里面并不存在這種類型。實際上,SignedSmall來自己V8中的一種優化策略,它表示在程序中經常使用的小的有符號整數(V8將高位的32位表示整數,低位的全部置0來表示SignedSmall),這種類型能夠獲得特殊處理(其他JavaScript引擎也有類似的優化策略)。

「值的表示」

V8通常使用一種叫做指針標記(Pointer Tagging)的技術來表示值,應用這種技術,V8在每個值里面都設置一個標識。我們處理的大部分值都分配在JavaScript堆上,并且由垃圾回收器(GC)來管理。但是對某些值來說,總是將它們分配在內存里開銷會很大。尤其是對于小整數,它們通常會用在數組索引和一些臨時計算結果。

圖片

在V8中存在兩種指針標識類型:分別是是Smi(即 Small Integer的縮寫)和堆對象( HeapObject,就是JavaScript的引用類型),其中堆對象是分配在內存的堆中,圖中的地址即指向堆中的某塊地方。

我們用最低有效位來區分堆對象(標志是1)和小整數(標志是0)。對于64位結構上的Smi,至少有32位有效位(低半部)是一直被置為0。另外32位,也就是Word的上半部,是被用來儲存32位有符號小整數的值。

僅僅是一次的執行,還不足以讓引擎這么快下定決心,相信add 函數隨后的執行都是Smi 類型。那么我們先來看看,如果在隨后的執行中,我們傳入不一樣的類型會怎么樣。

反饋向量的變化

反饋類型SignedSmall是指所有能用小整數表示的值。對于add操作而言,這意味著目前為止它只能看到輸入類型為Smi,并且所產生的輸出值也都是Smi(也就是說,所有的值都沒有超過32位整數的范圍)。下面我們來看看,當我們調用add的時候傳入一個不是Smi的值會發生什么。

function add(x, y) {
return x + y;
}
console.log(add(1, 2));
console.log(add(1.1, 2.2));
//調用100ci
%DebugPrint(add);

在d8加入命令 –allow-natives-syntax ,然后看到下面結果。

$ out/Debug/d8 --allow-natives-syntax add.js
DebugPrint: 0xb5101ea9d89: [Function] in OldSpace

- feedback vector: 0x3fd6ea9ef9: [FeedbackVector] in OldSpace
- length: 1
SharedFunctionInfo: 0x3fd6ea9989 <SharedFunctionInfo add>
Optimized Code: 0
Invocation Count: 2
Profiler Ticks: 0
Slot #0 BinaryOp BinaryOp:Number

首先,我們看到調用次數現在是2,因為運行了兩次函數add。然后發現BinaryOp 槽的值現在變成了Number,這表明對于這個加法已經有傳入了任意類型的數值(即非整數)。此外,這有一個反饋向量的狀態遷移圖,大致如下所示:

圖片

反饋狀態從 None 開始,這表明目前還沒有看到任何輸入,所以什么都不知道。狀態Any表明我們看到了不兼容的(比如number和string)輸入和輸出的組合。狀態Any意味著Add(字節碼中的)是多態。相比之下,其余的狀態表明Add都是單態(monomorphic),因為看到的輸入和產生的都是相同類型的值。下面是圖中名詞解釋:

  • SignedSmall 表示所有的值都是小整數(有效數值為是32位或者31位,取決于Word的在不同架構上的大小),均表示為Smi。
  • Number 表明所有的值都常規數字 (這包括小整數)。
  • NumberOrOddball 包括其他能被轉換成 Number 的 undefined, null, true 和 false 。
  • String :所有輸入值都是字符串
  • BigInt 表示輸入都是大整數。

需要注意一點,反饋只能在這個圖中前進(從 None 到 Any),不能回退。如果真的那樣做,那么我們就會有陷入去優化循環的風險。那樣情況下,優化編譯器發現輸入值與之前得到反饋內容不同,比如之前解釋器生成的反饋是 Number,但現在輸入值出現了 String,這時候已經生成的反饋和優化代碼就會失效,并回退到解釋器生成的字節碼版本。當下一次函數再次變熱(hot,多次運行),我們將再次優化它,如果允許回退,這時候優化編譯器會再次生成相同的代碼,這意味著會再次回到 Number 的情況。如果這樣無限制的回退去優化,再優化,編譯器將會忙于優化和去優化,而不是高速運行 JavaScript 代碼。

優化管道(The Optimization Pipeline)

現在我們知道了解釋器Ignition 是如何為函數add收集反饋,下面來看看優化編譯器如何利用反饋生成最小的代碼,因為_越小的機器指令代碼塊,意味著更快的速度_。為了觀察,我將使用一個特殊的內部函數OptimizeFunctionOnNextCall()在特定的時間點觸發V8對函數的優化。我們經常使用這些內部函數以非常特定的方式對引擎進行測試。

function add(x, y) {
return x + y;
}
add(1, 2); // Warm up with SignedSmall feedback.
%OptimizeFunctionOnNextCall(add);
add(1, 2); // Optimize and run generated code

在這里,給函數add傳遞兩個整數型值來明確call site “x + y”的反饋會被預熱為小整數(表示_這個call site全部傳遞的都是小整數,對于優化引擎來說將來得到的輸入也會是小整數_),并且結果也是屬于小整數范圍。然后我們告訴V8應該在下次調用函數add的時候去優化它(用TurboFan ),最終再次調用add,觸發優化編譯器運行生成機器碼。

圖片

TurboFan 拿到之前為函數add生成的字節碼,并從函數add的反饋向量表里提取出相關的反饋。優化編譯器將這些信息轉換成一個圖表示,再將這個圖表示傳遞給前端,優化以及后端的各個階段(見上圖)。在本文不會詳細展開這部分內容,這是另一個系列的內容了。我們要了解的是最終生成的機器碼,并看看優化推測是如何工作的。你可以在d8中加上命令 –print-opt-code來查看由TurboFan 生成的優化代碼。

圖片

這是由TurboFan 在x64架構上生成的機器碼,這里省略了一些無關緊要的技術細節(,下面就來看看這些代碼做了什么。

# Prologue
leaq rcx,[rip+0x0]
movq rcx,[rcx-0x37]
testb [rcx+0xf],0x1
jnz CompileLazyDeoptimizedCode
push rbp
movq rbp,rsp
push rsi
push rdi
cmpq rsp,[r13+0xdb0]
jna StackCheck

第一段代碼檢查對象是否仍然有效(對象的形狀是否符合之前生成機器碼的那個),或者某些條件是否發生了改變,這就需要丟棄這個優化代碼。這部分具體內容可以參考 Juliana Franco 的 “Internship on Laziness“。一旦我們知道這段代碼仍然有效,就會建立一個棧幀并且檢查堆棧上是否有足夠的空間來執行代碼。

# Check x is a small integer
movq rax,[rbp+0x18]
test al,0x1
jnz Deoptimize
# Check y is a small integer
movq rbx,[rbp+0x10]
testb rbx,0x1
jnz Deoptimize
# Convert y from Smi to Word32
movq rdx,rbx
shrq rdx, 32
# Convert x from Smi to Word32
movq rcx,rax
shrq rcx, 32

然后從函數主體開始。我們從棧中讀取參數x和y的值(相對于幀指針rbp,比如rbp+1這樣的地址,請參考棧幀概念),然后檢查兩個參數是否都是 Smi 類型(因為根據“+”得到的反饋,兩個輸入總是Smi)。這一步是通過測試最低有效位來完成。一旦確定了參數都是Smi,我們需要將它轉換成32位表示,這是通過將值右移32位來完成的。如果x或y不是Smi,則會立即終止執行優化代碼,接著負責去優化的模塊就會恢復到之前解釋器生成的函數add的代碼(即字節碼)。

# Add x and y (incl. overflow check)
addl rdx,rcx
jo Deoptimize
# Convert result to Smi
shlq rdx, 32
movq rax,rdx
# Epilogue
movq rsp,rbp
pop rbp
ret 0x18

然后我們繼續執行對輸入值的整數加法,這時需要明確地測試溢出,因為加法的結果可能超出32位整數的范圍,在這種情況下就要返回到解釋器版本,并在隨后將add的反饋類型提升為Number(之前說過,反饋類型的改變只能前進)。

最后我們通過將帶符號的32位值向上移動32位,將結果轉換回Smi表示,并將結果返回存到累加器rax 。

我們現在可以看到生成的代碼是高度優化的,并且適用于專門的反饋。它完全不去處理其他數字,字符串,大整數或任意JavaScript對象,只關注目前為止我們所看到的那種類型的值。這是使許多JavaScript應用程序達到最佳性能的關鍵因素。

為什么需要TypeScript?

在上面的介紹中,我們竭力避免了對JavaScript 對象的訪問,如果有對象加入,這將會變成一個很復雜的話題。但為了更好的展開這個話題,我們還是需要提一下,關于對象的優化是V8中極其重要的一部分。例如,以下面這個對象為例

var o = {
x: ''
}
var o1 = {
x: ''
y
}
//o1. o2

對于像 o.x這樣的屬性訪問,若o始終具有相同的形狀(形狀同結構,即相同的屬性以及屬性是相同的順序,例如o的結構一直是{x:v},其中v的類型是String),我們會把如何獲得o.x的過程信息緩存起來,構造成一個隱藏類( Hidden Class)。在隨后執行相同的字節碼時,不需要再次搜索對象o中x的位置。這種底層實現被稱為內聯緩存– inline cache (IC)。

你可以在Vyacheslav Egoro寫的這篇文章 “What’s up with monomorphism?” 中了解更多關于ICs和屬性訪問的細節。

總而言之,你現在應該了解到,作為一門弱類型的語言,從最早的SELF和smalltalk 語言開始,研究者就在不斷去優化這種弱類型語言的執行效率。

「從執行的角度來說,動態類型性能瓶頸很大程度是因為它的動態的類型系統,與靜態類型的語言相比, JavaScript 程序需要額外的操作來處理類型的動態性,所以執行效率比較低。」

說了這么多,最關鍵的一點

「確定你的代碼將要看到的類型很重要」

再加上另外一句話:

「作為動態語言,你的程序可能在90%的時間里,都在處理和代碼邏輯無關的事情。即:確認你的代碼是什么形狀」

從傳統的JavaScript 角度來說。

function add(x, y) {
return x + y;
}

你無法很好的保證 add 函數將要看到的類型,哪怕你確實想要這么做。但在一個大型的系統中,維護每一個函數和對象的形狀,極其困難。

你可能在前99次都保證了add 看到的都是Smi 類型,但是在第100次,add 看到了一個String,而在這之前,優化編輯器,即TurboFan,已經大膽的推測了你的函數只會看到Smi,那么這時候

Ops!

優化編輯器將不得不認為自己做了個錯誤的預測,它會立即把之前的優化丟掉。從字節碼開始重新執行。

而如果你的代碼一直陷入優化<->去優化的怪圈,那么程序執行將會變慢,慢到還不如不優化。

大多數的瀏覽器都做了限制,當優化/去優化循環發生的時候會嘗試跳出這種循環。比如,如果 JIT 做了 10 次以上的優化并且又丟棄的操作,那么就不繼續嘗試去優化這段代碼。

圖片

所以,到這里你應該明白了,有兩點準則:

  1. 「確保你的代碼是什么形狀很重要」

但比第一條更重要的是:

  1. 「確保你的代碼固定在某個形狀上」

而編寫TypeScript ,從工程和語言的層面上幫助你解決了這兩個準則,你可以暢快的使用TypeScript,而無需擔心你是否不小心違背了上面兩條準則。

責任編輯:姜華 來源: Tecvan
相關推薦

2017-12-17 16:34:18

JavaScript代碼V8

2021-05-28 05:30:55

HandleV8代碼

2022-06-02 12:02:12

V8C++JavaScript

2022-04-29 08:00:51

V8垃圾回收

2020-10-25 08:22:28

V8 引擎JavaScript回調函數

2022-06-21 08:52:47

Node.js服務端JavaScript

2022-02-25 08:32:07

nodemon搭Node.jsJavascript

2023-10-10 10:23:50

JavaScriptV8

2020-09-27 07:32:18

V8

2009-08-21 10:09:02

Google ChroV8引擎linux系統

2009-07-20 09:36:04

谷歌瀏覽器安全漏洞

2022-11-04 07:12:24

JavaScript基準測試

2010-07-20 16:35:52

V8JavaScript瀏覽器

2023-06-07 16:00:40

JavaScriptV8語言

2023-06-05 16:38:51

JavaScript編程語言V8

2022-09-16 08:32:25

JavaC++語言

2024-06-27 11:22:34

2014-11-26 09:51:24

GithubGoogleV8

2010-08-31 11:42:03

DB2MDC

2020-10-12 06:35:34

V8JavaScript
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 青青草一区| 成人性视频免费网站 | 孰女乱色一区二区三区 | 99视频在线免费观看 | 在线看av网址 | 九九av | 日韩视频专区 | 欧美精品二区 | 99久久国产综合精品麻豆 | 久久亚洲春色中文字幕久久久 | 欧美综合在线视频 | 国产精品免费高清 | 女同久久另类99精品国产 | 亚洲精品久久久久久一区二区 | 日韩精品成人网 | 黄视频网站在线 | 亚洲精品一级 | 欧美精品一区在线发布 | 国产亚洲精品精品国产亚洲综合 | 成人午夜影院 | 一区二区三区不卡视频 | 精品国产第一区二区三区 | 99久久精品免费看国产四区 | 婷婷久久综合 | 亚洲第一网站 | 一区二区三区四区在线免费观看 | 成人黄色在线视频 | 玖玖操 | 久久久福利 | 亚洲成人毛片 | 91se在线| 欧美一级大片 | 91久久婷婷 | 狠狠操你 | 热久久免费视频 | 国产欧美精品一区二区 | 一区二区中文 | 亚洲视频在线观看 | 国产亚洲成av人片在线观看桃 | 免费 视频 1级 | 亚洲精品在线看 |