NEON加速之memcpy在ARM平臺(tái)的優(yōu)化

0x01 前言

系統(tǒng)里面經(jīng)常需要大量地搬運(yùn)數(shù)據(jù),一般調(diào)用的都是memcpy() C庫來實(shí)現(xiàn)括堤,因此本著“揪牛角尖”的精神厉熟,我們就來探究探究加速方案廉丽!畢竟很多事情被分解到底層之后就是一樣的呢!

加速這個(gè)玩意弄兜,其實(shí)是跟很多因素相關(guān)的药蜻,因此我們要就環(huán)境來論加速瓷式,把當(dāng)前環(huán)境考慮進(jìn)去然后再設(shè)計(jì)出合理的優(yōu)化策略,這才是萬全之策谷暮!也即魯棒性很強(qiáng)的策略蒿往。

這里我們從上而下地談?wù)刴emcpy的優(yōu)化問題,一個(gè)問題可以如此優(yōu)化湿弦,那么相比其他問題也都是類似罷瓤漏!這怕就是深度學(xué)習(xí)之遷移學(xué)習(xí)的由來了!這個(gè)世界真奇妙颊埃!

0x02 測(cè)試環(huán)境

我們?cè)贏RM Cortex-A8 環(huán)境下進(jìn)行一系列的測(cè)試蔬充。

測(cè)試時(shí)基于源/終地址以及我們要讀寫的byte數(shù)都是L1(64byte)的倍數(shù);

我們需要考慮下對(duì)齊班利,但是這里只測(cè)了16MB饥漫,影響不大故不考慮;

測(cè)試時(shí)間是由處理器內(nèi)部的性能寄存器記錄的罗标;

所有的測(cè)試中庸队,L1 NEON 位被激活,這意味著當(dāng)我們使用Neon 加載(load)指令的時(shí)候闯割,會(huì)使得L1數(shù)據(jù)緩存進(jìn)行l(wèi)inefill操作彻消;

我們把分別對(duì)應(yīng)指令和數(shù)據(jù)的L1、L2緩存使能宙拉,同時(shí)MMU核分支預(yù)測(cè)同樣也被使能了宾尚。

有些地方甚至還使用到了PLD預(yù)取指令:這條指令會(huì)使得L2緩存在這個(gè)數(shù)據(jù)被使用前的某個(gè)時(shí)間開始加載數(shù)據(jù),它提前發(fā)出了內(nèi)存請(qǐng)求谢澈,所以CPU就不需要在那傻等memory把數(shù)據(jù)給吐出來了煌贴!

0x03 測(cè)試策略

策略1: 傻搬

傻搬就是使用常規(guī)匯編指令一個(gè)字一個(gè)字地拷貝。

如下匯編代碼所示锥忿, 我們每次把地址值加個(gè)4牛郑,然后不斷循環(huán)直至搬運(yùn)結(jié)束,這是最基本最常規(guī)的一種操作了敬鬓,因此我們 把這個(gè)時(shí)間作為一個(gè)baseline.

匯編代碼如下:

WordCopy
      LDR r3, [r1], #4
      STR r3, [r0], #4
      SUBS r2, r2, #4
      BGE WordCopy

策略2: 多加載指令

我們之前是只用到了LDR指令井濒,一次搬運(yùn)32bit也就是1個(gè)word,由于只用到了r0~r3寄存器列林,因此每次調(diào)用的時(shí)候無需進(jìn)行堆棧操作瑞你;

這里我們使用LDM核STM指令(M是Multiple的簡(jiǎn)多,意味著一次操作多個(gè))希痴,每個(gè)迭代過程中能夠操作8word數(shù)據(jù)者甲,由于額外寄存器的使用,因此我們需要有現(xiàn)場(chǎng)保護(hù)操作砌创,也就是入棧出棧操作虏缸。

LDMCopy
      PUSH {r4-r10}
LDMloop
      LDMIA r1!, {r3 - r10}
      STMIA r0!, {r3 - r10}
      SUBS r2, r2, #32
      BGE LDMloop
      POP {r4-r10}

注:
1鲫懒、 r0~r3一般作為函數(shù)的局部變量,傳入的函數(shù)的參數(shù)按照順序分給他們四個(gè)刽辙,超出的就要進(jìn)入堆棧區(qū)了窥岩,其中r0一般還會(huì)作為函數(shù)返回值變量
2宰缤、 r1!表示從r1這個(gè)地址處連續(xù)搬數(shù)據(jù)至r3到r10颂翼,每搬一個(gè),r1的值就會(huì)自動(dòng)加4慨灭。

策略3: NEON搬運(yùn)

常規(guī)的NEON搬運(yùn)朦乏,具體指令啊信息啊什么的,怎么操作啊氧骤,去看我上一篇博客吧呻疹!

NEONCopy
      VLDM r1!, {d0-d7}
      VSTM r0!, {d0-d7}
      SUBS r2, r2, #0x40
      BGE NEONCopy

這里一個(gè)d寄存器就是64bit,2個(gè)word筹陵,8個(gè)d寄存器搬運(yùn)的是16個(gè)word了肮舸浮!報(bào)告老板朦佩,有人開掛并思!

策略4: 傻搬+預(yù)取

如題。

  • PLD的意思是我從r1地址處開始預(yù)先取出256byte數(shù)據(jù)到cache里面吕粗;
  • r12表示的是 我要搬運(yùn)16次,每次都是4個(gè)byte旭愧,共計(jì)搬運(yùn)64byte每輪颅筋;
  • 然后每輪結(jié)束后,把r2中的計(jì)數(shù)器減去64byte(0x40)開始下一輪直至結(jié)束输枯;
WordCopyPLD
     PLD [r1, #0x100]
     MOV r12, #16
WordCopyPLD1
     LDR r3, [r1], #4
     STR r3, [r0], #4
     SUBS r12, r12, #1
     BNE WordCopyPLD1
     SUBS r2, r2, #0x40
     BNE WordCopyPLD

同樣的tips:這里每輪預(yù)取取多了议泵!

策略5: 多加載+預(yù)取

這里的優(yōu)勢(shì)是。桃熄。先口。如題~

LDMCopyPLD
    PUSH {r4-r10}
LDMloopPLD
    PLD [r1, #0x80]
    LDMIA r1!, {r3 - r10}
    STMIA r0!, {r3 - r10}
    LDMIA r1!, {r3 - r10}
    STMIA r0!, {r3 - r10}
    SUBS r2, r2, #0x40
    BGE LDMloopPLD
    POP {r4-r10}

策略6: NEON + PLD

  • 預(yù)取192byte;
  • d0~d7共計(jì)8x64bit=64byte

這里計(jì)算剛剛好瞳收,都很完美自洽碉京!

NEONCopyPLD
     PLD [r1, #0xC0]
     VLDM r1!,{d0-d7}
     VSTM r0!,{d0-d7}
     SUBS r2,r2,#0x40
     BGE NEONCopyPLD

策略7: Mixed ARM and NEON memory copy with preload

也就是說把各種指令穿插在一起組合處一個(gè)“多元體”來試驗(yàn)看看是不是會(huì)速度更快咯~

ARMNEONPLD
      PUSH {r4-r11}
      MOV r3, r0
ARMNEON
      PLD [r1, #192]
      PLD [r1, #256]
      VLD1.64 {d0-d3}, [r1@128]!
      VLD1.64 {d4-d7}, [r1@128]!
      VLD1.64 {d16-d19}, [r1@128]!
      LDM r1!, {r4-r11}
      SUBS r2, r2, #128
      VST1.64 {d0-d3}, [r3@128]!
      VST1.64 {d4-d7}, [r3@128]!
      VST1.64 {d16-d19}, [r3@128]!
      STM r3!, {r4-r11}
      BGT ARMNEON
      POP {r4-r11}

測(cè)試時(shí)間結(jié)果

測(cè)試算法 時(shí)間花銷(ms) 加速比
傻搬 104.8 100%
多加載(指令) 94.5 111%
NEON搬 104.8 100%(說明等待時(shí)間占主要比例)
傻搬+預(yù)取 137.5 76%
多加載+預(yù)取 106.6 98%
NEON+預(yù)取 70.2 149%
指令大雜燴 93.5 112%

小結(jié):有一些奇怪的結(jié)論。

多加載指令僅僅提升了11%的性能螟深,但是我們沒有那么多指令了啊同時(shí)指令少就代表分枝預(yù)測(cè)里面的分支較少靶持妗!原因是:

  • 指令cache100%擊中界弧,因此取指令是無須等待的凡蜻;
  • 分枝預(yù)測(cè)在這里也不需要預(yù)測(cè)傻按钭邸!
  • 單個(gè)寫(一個(gè)接一個(gè)地寫)划栓,memory system也把它當(dāng)成突發(fā)寫了兑巾,所以說效率并沒有顯著提升;

NEON指令居然沒有提升讀寫速度:

  • 讀寫循環(huán)的執(zhí)行使用的寄存器很少忠荞,因此存在寄存器數(shù)據(jù)沖突的可能性就薪琛;
  • 因?yàn)榧拇嫫饔玫纳僮耆鳎虼颂貏e適合搬小數(shù)據(jù)塊奋姿,因?yàn)槲覀儾恍枰褩2僮鱽砘謴?fù)現(xiàn)場(chǎng)啊K乇辍3剖!
  • Cortex-A8處理器可以配置NEON加載數(shù)據(jù)的時(shí)候加載到L2 cache头遭;可以防止內(nèi)存copy過程中把L1中的不用數(shù)據(jù)給替換掉寓免;(?计维?袜香??)

盡管上面bb了一大堆好處鲫惶,但是實(shí)踐證明效果不咋地蜈首!
(這里存疑,我在A53平臺(tái)試驗(yàn)過欠母,大概會(huì)塊三倍的樣子盎恫摺!除非時(shí)間是異或操作的時(shí)間I吞省踩寇?)

PLD可以使得內(nèi)存控制器在數(shù)據(jù)被使用前就取到;因此加上NEON如虎添翼六水。

  • 其次俺孙,我們知道在burst傳輸?shù)臅r(shí)候,第一次接入的延時(shí)是很大的掷贾,因此我們可以發(fā)起多次地請(qǐng)求到控制器(當(dāng)然得控制器夠先進(jìn)夠高級(jí))睛榄,這樣子控制器就會(huì)把后面的請(qǐng)求合到一起,從而把后面每一次的請(qǐng)求的接入延時(shí)相當(dāng)于去掉了想帅,第一個(gè)access latency均攤給每個(gè)request之后也忽略不計(jì)了懈费,這樣子好高效啊博脑!

影響內(nèi)存拷貝速度的因素

因素1: 要拷貝的數(shù)據(jù)量

有些實(shí)現(xiàn)需要一定的準(zhǔn)備時(shí)間憎乙,然后搬起來了就老快了票罐。

因此搬運(yùn)大數(shù)據(jù)塊的時(shí)候,可以把建立準(zhǔn)備的時(shí)間均攤泞边,因此還好该押,但是搬運(yùn)小塊數(shù)據(jù)的時(shí)候就不劃算了喲!

比如:在函數(shù)的開始stacking許多的寄存器阵谚,然后在主循環(huán)里面使用LDM跟STM指令來操作多個(gè)寄存器蚕礼;

因素2: 對(duì)齊Alignment

ARM架構(gòu)搬運(yùn)word對(duì)齊的數(shù)據(jù)會(huì)更高效;

courser alignment granularities這玩意能支撐性能梢什;

多加載指令在Cortex-A8 能每個(gè)周期從L1 cache加載2個(gè)寄存器的值奠蹬,但是只有地址是64位對(duì)齊的時(shí)候才可以。(所以NEON雖然一次可以搬運(yùn)128bit嗡午,但是你的CPU 位寬囤躁,DRAM位寬都是32bit的,因此數(shù)據(jù)最終還是被拆分成一個(gè)一個(gè)的32bit再存儲(chǔ)的荔睹,而我們的程序確實(shí)加速了是因?yàn)楫惢虿僮髂遣糠謺r(shí)間加速了袄暄荨!)

cache對(duì)齊也是有影響的捌宵距!
Cache behaviour (discussed later) can affect the performance of data accesses depending on its alignment relative to the size of a cache line. For the Cortex-A8, a level 1 cache line is 64 bytes, and a level 2 cache line is 64 bytes.

因素3: Memory特性

這里討論的操作(見上述諸程序)其性能瓶頸在存儲(chǔ)的接口部分。

因?yàn)槲覀兊难h(huán)很小啊吨拗,所以指令cache的就很好了满哪,而且并沒有計(jì)算部分(注意了,我們March C-代碼部分優(yōu)化是有數(shù)學(xué)運(yùn)算的劝篷,因此這部分比較費(fèi)時(shí))哨鸭,因此處理器的邏輯計(jì)算部分壓力并不大,因此速度因素極大地落在存儲(chǔ)器的速度上了携龟!

特定種類的memory在某種特定的讀寫模式下會(huì)性能更優(yōu)兔跌,比如SDRAM的burst傳輸需要一個(gè)很長(zhǎng)的延時(shí)來完成初始化操作勘高,但是一旦操作完成就能很快地完成后續(xù)的讀寫操作峡蟋;(我的MARCH算法加速部分把burst傳輸模式考慮進(jìn)去

此外一個(gè)好的memory控制器是能夠并行接受很多讀寫請(qǐng)求的,并把這個(gè)initial latency給均攤掉华望。

此外一些特定的代碼讀寫順序也可能改善性能蕊蝗;

因素4: cache的使用

大量數(shù)據(jù)搬運(yùn)的時(shí)候,很顯然會(huì)把cache里面的數(shù)據(jù)全部“換血”的赖舟;

盡管這在內(nèi)存自身拷貝的時(shí)候不會(huì)有什么影響蓬戚,但是它可能會(huì)減速后面的代碼,最終降低整體的性能宾抓。

因素5: Code dependencies

在標(biāo)準(zhǔn)的 memcpy()函數(shù)運(yùn)行時(shí)子漩,尤其遇上慢速的memory時(shí)豫喧,處理器大部分時(shí)間都沒有被使用。

因此我們可以考慮在memcopy期間運(yùn)行一些其他的代碼幢泼;

因?yàn)閙emcpy()時(shí)阻塞的紧显,因此只有函數(shù)結(jié)束才會(huì)返回,而此時(shí)cpu時(shí)被占死了缕棵;
我們可以使用管道來實(shí)現(xiàn)孵班,把memcpy()放倒后臺(tái)運(yùn)行,然后通過poll或者中斷來隨時(shí)監(jiān)控內(nèi)存搬運(yùn)的情況

  • 使用DMA操作招驴,這樣完全解放CPU了篙程;并把數(shù)據(jù)塊打碎這樣就能一邊搬運(yùn)一遍操作了!效率提高了呢1鹄濉(都是一些很常規(guī)的想法呢J觥)

  • cortex-A8內(nèi)置的預(yù)加載引擎
    數(shù)據(jù)預(yù)加載到L2 cache;

我們CPU先啟動(dòng)預(yù)加載指令丹允,然后就去干別的活郭厌,直到接到電話(中斷)說加載完成了,那么我就可以去對(duì)它進(jìn)行操作了雕蔽,操作完之后繼續(xù)下一輪折柠;

  • 使用其他處理器
    內(nèi)嵌的其他核原理同DMA;

附錄 PLD基本思想及應(yīng)用考量

當(dāng)我們?cè)诎沧科脚_(tái)需要處理圖像數(shù)據(jù)的時(shí)候批狐,其中一個(gè)基本的操作就是把大量的數(shù)據(jù)從內(nèi)存搬來搬去扇售,相對(duì)于CPU來說這個(gè)很耗時(shí)間,除了NEON加速之外嚣艇,上面提到的PLD加速也是很有效的承冰,具體多有效我們看實(shí)例。

我們比如在處理攝像頭數(shù)據(jù)的時(shí)候食零,內(nèi)存中搬數(shù)據(jù)一般是這樣寫的:

while (n--) {
    *dest++ = *src++;
}

在我當(dāng)前平臺(tái)上跑了一下困乒,1MB的空間搬完需要25ms的樣子,這個(gè)就很費(fèi)時(shí)了贰谣,究其根本娜搂,是因?yàn)閿?shù)據(jù)不在處理器的cache中,因此CPU需要花時(shí)間來等你DRAM傳過來吱抚,也就是說捐康,是我當(dāng)前的CPU帶寬大于存儲(chǔ)器的帶寬了忠聚。

因此解決方案就是我們提前把數(shù)據(jù)放到cache中,由于cache帶寬大于DRAM小于CPU,因此可以減少等待時(shí)間敦锌。

改進(jìn)后的代碼如下所示,提前預(yù)取數(shù)據(jù),也就是在真正搬運(yùn)數(shù)據(jù)之前目標(biāo)數(shù)據(jù)就被放倒cache中來了,等真正搬數(shù)據(jù)時(shí)涮坐,一下子就在cache中hit了,馬上走你誓军!效率快很多膊升!

這里有個(gè)小細(xì)節(jié),每次只搬運(yùn)了32bit也就是4byte谭企,但是我們預(yù)取了128byte廓译,這樣子豈不是cache很快就溢出了?關(guān)于溢出CPU會(huì)怎么處理债查,我就沒深究了非区,這也不是這段代碼的主旨,權(quán)當(dāng)留下我自己的一個(gè)思考吧盹廷!

這個(gè)酒厲害了征绸,時(shí)間變?yōu)?ms了,提速了三倍之多俄占!

while (n--) {
    asm ("PLD [%0, #128]"::"r" (src));
    *dest++ = *src++;
}

當(dāng)然了管怠,優(yōu)化這種事情時(shí)做不完的,你可以隨著環(huán)境的變換不斷優(yōu)化的:

  • 比如這里缸榄,我們預(yù)取后存在溢出問題渤弛,也就是說沒有物盡其用;
  • 其次甚带,循環(huán)里面就一句搬運(yùn)指令她肯,然后就是n--操作,判斷操作等鹰贵,這樣子來看的話一個(gè)loop里面真正干活的指令占的比例很小晴氨,因此也是一種浪費(fèi),把資源浪費(fèi)在一些不重要的事情上了碉输!

算法里面又個(gè)概念籽前,就是時(shí)間跟空間是可以互相轉(zhuǎn)換的,在這里的表現(xiàn)就是我通過多寫一些代碼操作敷钾,從而將有效操作時(shí)間在每個(gè)loop中的比例提升上來枝哄。

如下所示,我每個(gè)loop里面增加3個(gè)搬運(yùn)操作闰非,這樣就保證了cache不會(huì)溢出膘格,同時(shí)每個(gè)loop中數(shù)據(jù)搬運(yùn)部分所占的CPU時(shí)間比例提升了峭范,也即“有效功率”提升了财松!

這樣子做大概又加快了1ms的樣子!

n /= 4; //assume it's multiple of 4
while (n--) {
    asm ("PLD [%0, #128]"::"r" (src));
    *dest++ = *src++;
    *dest++ = *src++;
    *dest++ = *src++;
    *dest++ = *src++;
}

思路總結(jié)

這個(gè)世界是由基本的元素組成的,操作系統(tǒng)是由一些基本的門電路架起來的辆毡,雪花是分形的菜秦,因此我們總能在深入一個(gè)案例后,發(fā)現(xiàn)一些普世的道理舶掖!共勉球昨!

我說過:“優(yōu)化時(shí)可以隨著環(huán)境的變化一直做的!”(名言罢H痢主慰!記住啊鲫售!劃重點(diǎn)共螺!要考的!)

這里不是有四個(gè)搬運(yùn)操作嘛情竹!編譯器優(yōu)化后也不知道會(huì)優(yōu)化成啥樣子藐不,我們可以直接用匯編嘛!匯編里面的四個(gè)LDR/STR指令秦效,然后是LDRM/STRM可以節(jié)省三次CPU指令操作時(shí)間及等待時(shí)間雏蛮,然后就是NEON的并行加速了!哎呀呀~不得了阱州!只會(huì)越來愈快挑秉!具體多快!你自己去實(shí)驗(yàn)咯苔货!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衷模,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蒲赂,更是在濱河造成了極大的恐慌阱冶,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滥嘴,死亡現(xiàn)場(chǎng)離奇詭異木蹬,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)若皱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門镊叁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人走触,你說我怎么就攤上這事晦譬。” “怎么了互广?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵敛腌,是天一觀的道長(zhǎng)卧土。 經(jīng)常有香客問我,道長(zhǎng)像樊,這世上最難降的妖魔是什么尤莺? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮生棍,結(jié)果婚禮上颤霎,老公的妹妹穿的比我還像新娘。我一直安慰自己涂滴,他們只是感情好友酱,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著柔纵,像睡著了一般粹污。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上首量,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天壮吩,我揣著相機(jī)與錄音,去河邊找鬼加缘。 笑死鸭叙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拣宏。 我是一名探鬼主播沈贝,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼勋乾!你這毒婦竟也來了宋下?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤辑莫,失蹤者是張志新(化名)和其女友劉穎学歧,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體各吨,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枝笨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了揭蜒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片横浑。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖屉更,靈堂內(nèi)的尸體忽然破棺而出徙融,到底是詐尸還是另有隱情,我是刑警寧澤瑰谜,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布欺冀,位于F島的核電站树绩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏脚猾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一砚哗、第九天 我趴在偏房一處隱蔽的房頂上張望龙助。 院中可真熱鬧,春花似錦蛛芥、人聲如沸提鸟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽称勋。三九已至,卻和暖如春涯竟,著一層夾襖步出監(jiān)牢的瞬間赡鲜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工庐船, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留银酬,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓筐钟,卻偏偏與公主長(zhǎng)得像揩瞪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子篓冲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361