Go dilinde ertelemenin geçmişi ve bugünü

Yazar | Ou Changkun

Kaynak | Ma Nong Tao Huayuan

Erteleme ifadesi en eski Go dili tasarımında yoktu ve bu özellik daha sonra eklendi. Robert Griesemer dil belirtim yazımını tamamladı ve Ken Thompson ilk uygulamayı tamamladı. İkisi dili tamamlamak için işbirliği yaptı karakteristik.

Erteleme işlevinin semantiği, işlev döndüğünde, panik yaptığında veya runtime.Goexit olduğunda çağrılacağını belirtir. Sezgisel olarak, erteleme derleyici tarafından doğrudan çağrı yerine eklenmelidir. Derleme zamanı özelliği gibi görünmektedir ve çalışma zamanı performans sorunları olmamalıdır. C ++ RAII paradigmasına çok benzer (kaynakların rolünden ayrılırken) Etki alanı, yıkıcı otomatik olarak yürütülür). Ancak gerçek durum, erteleme, bağımlı kaynaklarıyla bağlantılı olmadığından, aynı zamanda koşullar ve döngü ifadelerinde görünmesine de izin verilmesidir, bu nedenle artık erteleme anlamını nispeten karmaşık hale getiren kapsamla ilgili bir kavram değildir. Bazı karmaşık durumlarda, derleme zamanında kaç tane erteleme çağrısının bulunduğunu belirlemek imkansızdır.

Örneğin, belirsiz sayıda yürütme içeren bir for döngüsünde, erteleme yürütme sayısı rastgeledir:

1func randomDefers { 2 rand.Seed (time.Now.UnixNano) Rand.Intn için 3 (100) > 42 { 4 erteleme işlevi { 5 println ("changkun.de/golang") 6} 7} 8}

Bu nedenle, erteleme bedava bir öğle yemeği değildir.Karmaşık bir aramada, üretilmesi gereken gecikmeli aramaların sayısı doğrudan belirlenemediğinde, gecikmeli ifade işletim performansında bir düşüşe neden olacaktır. Bu yazıda, ertelemenin özünü ve ilgili performans optimizasyon yöntemlerini tartışacağız.

  • Erteleme türü

  • Yığın üzerinde ayrılan erteleme

    • Derleme aşaması

    • İşletme aşaması

  • Yığın üzerinde erteleme oluşturun

  • Açık kodlu erteleme

    • Üretim koşulları

    • Gecikme biti

  • Defer'ın optimizasyon yolu

  • özet

  • Daha fazla okuma için referanslar

Erteleme türü

Ertelenmiş bir cümlenin dilbilgisel üretimi DeferStmt- > "Erteleme" İfadesinin açıklaması çok basittir, bu yüzden onu bir sözdizimi ağacı biçiminde ele almak kolaydır, ancak burada daha çok ilgilendiğimiz şey, anlambiliminin arkasındaki ara ve nesne kodunun biçimidir.

"Go Language Original" Go Program Derleme Süreci bölümünde, ara kod oluşturma aşamasında compileSSA'nın önce SSA biçiminde işlev gövdesini oluşturmak için buildssa'yı çağıracağını ve ardından işlevin SSA ara temsilini dönüştürmek için genssa'yı çağıracağını belirtmiştik. Özel talimatlar için.

Go dili ifadelerinin buildssa aşamasının yürütülmesi sırasında, state.stmt, işlevdeki her bir ifadenin SSA işlemesini tamamlayacaktır.

1 // src / cmd / derleme / dahili / gc / ssa.go 2func buildssa (fn * Node, worker int) * ssa.Func { 3 değişken durum 4 ... 5 sn.stmtList (fn.Nbody) 6 ... 7} 8func (s * durumu) stmtList (l Düğümler) { _ İçin 9, n: = aralık l.Slice {s.stmt (n)} 10}

Gecikmeli cümleler için orta temsilde üç farklı gecikme türü vardır. Birincisi en genel durumdur Yığın ayırma Gecikmeli ifade, ikinciye izin verilir Yığın üzerinde tahsis Sonuncusu, gecikmiş ** Açık kodlu ** ifadesidir.

1 // src / cmd / derleme / dahili / gc / ssa.go 2func (s * durum) stmt (n * Düğüm) { 3 ... 4 anahtar n.Op { 5 vaka ODEFER: 6 // Açık kodlu erteleme 7 if s.hasOpenDefers { 8 s. OpenDeferRecord (n.Left) 9} else { 10 // yığın üzerinde ayrılan erteleme 11 d: = callDefer 12 if n.Esc == EscNever { 13 // yığına tahsis edilen erteleme 14 d = callDeferStack 15} 16 sn. Çağrı (n.Left, d) 17} 18 vaka ... 19} 20 ... yirmi bir}

Yığın üzerinde ayrılan erteleme

Öncelikle yığın üzerinde tahsis edilen en basit erteleme biçimini tartışalım. Yığın üzerindeki ayırmanın nedeni, erteleme ifadesinin döngü deyiminde görünmesi veya üst düzey derleyici optimizasyonunun gerçekleştirilememesidir.

Bir döngü deyiminde bir ve erteleme görünüyorsa, çalıştırılabilirlerin sayısı derleme zamanında belirlenemeyebilir; bir erteleme çağrısı çok fazla nedenden ötürü derleyici tarafından açık kodlanamazsa, öbek üzerinde erteleme de tahsis edilecektir. .

Kısacası, bu belirsizliğin varlığı nedeniyle, yığın üzerinde tahsis edilen erteleme, en fazla çalışma zamanı desteğini gerektirir ve bu da en büyük çalışma zamanı ek yüküyle sonuçlanır.

Derleme aşaması

Ertelenmiş deyimin işlevini dil belirtimine uygun hale getirmek için, ifade derlemenin SSA aşamasında iki konuya çevrilecektir.Birinci konu gecikmeli işlevin kendisidir ve diğer özne, işlev sona erdiğinde yürütülmesi gereken kaydedilmiş ertelemedir. Kod bloğu.

State.call çağrısı, gecikme çağrısı parametrelerini kaydetmek ve bir deferproc çağrısı talimatı oluşturmak için bir talimat üretir; daha sonra state.exit çağrısı, işlev dönmeden önce erteleme çağrısı talimatını ekler.

1 // src / cmd / derleme / dahili / gc / ssa.go 2func (s * durum) çağrı (n * Düğüm, k callKind) * ssa.Value { 3 ... 4 var çağrı * ssa.Value 5 if k == callDeferStack { 6 ... 7} else { 8 // Yığın üzerinde erteleme oluştur 9 argStart: = Ctxt.FixedFrameSize 10 // Parametreyi ertele 11 if k! = CallNormal { 12 // deferproc parametrelerini kaydedin 13 argsize: = s.constInt32 (types.Types, int32 (stksize)) 14 adres: = s.constOffPtrSP (s.f.Config.Types.UInt32Ptr, argStart) 15 s.store (types.Types, addr, argsize) // parametre boyutunu kaydet 16 adres = s.constOffPtrSP (s.f.Config.Types.UintptrPtr, argStart + int64 (Genişlikptr)) 17 s.store (types.Types, addr, closure) // fonksiyon adresini kaydet fn 18 standart boyut + = 2 * int64 (Genişlikptr) 19 argStart + = 2 * int64 (Genişlikptr) 20} yirmi bir ... yirmi iki 23 // deferproc çağrısı oluştur 24 anahtar { 25 durum k == callDefer: 26 çağrı = s.newValue1A (ssa.OpStaticCall, types.TypeMem, deferproc, s.mem) 27 ... 28} 29 ... 30} 31 ... 3233 // Erteleme bloğunu sonlandırın 34 if k == callDefer || k == callDeferStack { 35 s. Çıkış 36 ... 37} 38 ... 39} 40func (s * state) exit * ssa.Block { 41 if s.hasdefer { 42 if s.hasOpenDefers { 43 ... 44} başka { 45 // erteleme çağrısı 46 s.rtcall (Deferreturn, true, nil) 47} 48} 49 ... 50}

Örneğin, salt bir erteleme çağrısı için:

1 paket ana 23func foo { 4 dönüş 5} 67func main { 8 erteleme foo 9 dönüş 10}

Yığın üzerinde ayırma şeklinde derlenmeye zorlarsak, aşağıdaki montaj kodunu gözlemleyebiliriz. Bunların arasında, defer foo bir deferproc çağrısına dönüştürülür ve işlev dönmeden önce deferreturn çağrılır:

1TEXT main.foo (SB) /Users/changkun/Desktop/defer/ssa/main.go 2 dönüş 30x104ea20 c3 RET 45TEXT main.main (SB) /Users/changkun/Desktop/defer/ssa/main.go 6func main { 7 ... 8 // defer foo {...} öğesini bir deferproc çağrısına dönüştür 9 // deferproc'u çağırmadan önce parametreleri hazırlayın, bu örnekte parametre yok 100x104ea4d c7042400000000 MOVL $ 0x0, 0 (SP) 110x104ea54488d0585290200 LEAQ go.func. * + 60 (SB), AX 120x104ea5b 4889442408 MOVQ AX, 0x8 (SP) 130x104ea60 e8bb31fdff CALL runtime.deferproc (SB) 14 ... 15 // RET fonksiyon dönüş talimatından önce eklenen deferreturn ifadesi 160x104ea7b 90 NOPL 170x104ea7c e82f3afdff CALL runtime.deferreturn (SB) 180x104ea81488b6c2410 MOVQ 0x10 (SP), BP 190x104ea864883c418 ADDQ 0x18 $, SP 200x104ea8a c3 RET 21 // İşlev sonu 220x104ea8b e8d084ffff CALL çalışma zamanı.morestack_noctxt (SB) 230x104ea90 eb9e JMP main.main (SB)

İşletme aşaması

Bir işlevdeki gecikmiş ifade, bağlantılı bir _defer kayıtları listesi olarak kaydedilir ve bir Goroutine eklenir. _Defer kaydının spesifik yapısı da çok basittir, esas olarak çağrıda yer alan parametrelerin boyutu, geçerli erteleme ifadesinin bulunduğu işlevin PC ve SP kayıtları, ertelenen işlevin giriş adresi ve seri olarak birden fazla ertelemenin bağlantı listesi dahildir. Bağlantılı liste bir sonrakini işaret eder. Gerçekleştirilmesi gereken erteleme Şekil 9.2.1'de gösterilmektedir.

1 // src / runtime / panic.go 2type _defer struct { 3 boyut int324 yığın bool 5 sp uintptr 6 adet uintptr 7 fn * funcval 8 bağlantı * _defer 9 ... 10} 11 // src / runtime / runtime2.go 12type g struct { 13 ... 14 _defer * _defer 15 ... 16}

Goroutine'e eklenen _defer kayıtlarının bağlantılı listesi

Artık öbek üzerinde tahsis edilen bir ertelenmiş ifadenin, gecikmiş işlev çağrısını kaydetmek için deferproc'da derlendiğini biliyoruz; işlevin sonunda, ertelenmiş çağrıyı yürütmek için bir erteleme dönüş çağrısı eklenmiştir.

Bu iki aramaya ne olduğuna ayrıntılı bir şekilde bakalım.

Deferproc'un ilk biçimi olan deferproc'a bakalım. Bu çağrı çok basittir, sadece erteleme ile çağrılması gereken işlevi kaydeder:

1 // git: nosplit 2func deferproc (siz int32, fn * funcval) { 3 ... 4 sp: = getcallersp 5 argp: = uintptr (güvensiz.Pointer (fn)) + güvensiz.Sizeof (fn) 6 arayan bilgisayar: = getcallerpc 78 g: = newdefer (boyut) 9 d.fn = fn 10 d.pc = arayan bilgisayar 11 d.sp = sp 1213 // Parametreleri _defer kaydına kaydedin 14 anahtar boyutu { 15 vaka 0: // hiçbir şey yapma 16 vaka sys.PtrSize: 17 * (* uintptr) (deferArgs (d)) = * (* uintptr) (güvensiz.Pointer (argp)) 18 varsayılan: 19 memmove (deferArgs (d), güvensiz.Pointer (argp), uintptr (boyut)) 20} yirmi bir 22 dönüş0 yirmi üç}

Bu kodda, aslında sadece bazı basit parametre işlemlerini yapmaktadır.Örneğin, fn, erteleme tarafından çağrılan fonksiyonun çağıran adresini kaydeder ve siz, parametrelerinin boyutunu belirler. Ve newdefer aracılığıyla yeni bir _defer örneği oluşturmak için ve ardından fn, callerpc ve sp ertelemeyi çağıran Goroutine bağlamını kaydetmek için.

Burada parametreleri kopyalamak için bir işlem gördüğümüze dikkat edin. Bu işlem de pratikte yaşadığımız şeydir Erteleme çağrısı kaydedildiğinde parametre değerlendirilmez ancak parametrenin bir kopyası tamamlanır. Bunun nedeni anlamsal düşüncelerden kaynaklanmaktadır. Sezgisel olarak konuşursak, erteleme parametresi değerlendirme adımını geciktirmek yerine, geçirilen parametreyi yazıldığı konumda değerlendirmelidir, çünkü gecikmiş parametre değişebilir ve ertelemenin anlambiliminde beklenmedik hatalara neden olabilir. .

Örneğin, f, _: = os.Open ("dosya.txt") ve hemen f.Close ertelemesini belirtin.F'nin değeri aşağıdaki ifadeyle değiştirilirse, f normal olarak kapatılmaz.

Performans nedenlerinden ötürü, newdefer, P veya zamanlayıcı çizelgesindeki yerel veya global erteleme havuzu aracılığıyla yığın üzerinde tahsis edilen belleği yeniden kullanır. Ertelemenin kaynak havuzu, gecikmeli çağrının gerektirdiği parametrelere göre erteleme kaydının boyut düzeyini belirleyecek ve her 16 bayt bir düzeye bölünecektir. Bu yaklaşımın motivasyonu, çalışma zamanı bellek ayırıcısının farklı boyutlardaki nesneleri tahsis etme fikriyle aynıdır, bu yüzden burada derinlemesine tartışmayacağım.

1 // src / runtime / runtime2.go 2type p struct { 3 ... 4 // Farklı boyutlarda yerel erteleme havuzları 5 deferpool * _defer 6 deferpoolbuf * _defer 7 ... 8} 9type schedt struct { 10 ... 11 // Farklı boyutlarda global erteleme havuzu 12 deferlock mutex 13 deferpool * _defer 14 ... 15}

Yeni oluşturulan _defer örneği için, Goroutine tarafından ayrılmış erteleme listesine eklenecek ve bağlantı alanı aracılığıyla bağlanacaktır:

1 // src / runtime / panic.go 23 // git: nosplit 4func newdefer (siz int32) * _defer { 5 değişken d * _defer 6 sc: = deferclass (uintptr (siz)) 7 gp: = getg 8 // Erteleme parametresinin boyutunun doğrudan p'nin erteleme havuzundan tahsis edilip edilmediğini kontrol edin 9 eğer sc < uintptr (len (p {}. deferpool)) { 10 pp: = gp.m.p.ptr 1112 // p yerel olarak tahsis edilemiyorsa, P'nin yerel kaynak havuzunu doldurmak için küresel havuzdan ertelemenin yarısını alın 13 if len (pp.deferpool) == 0 sched.deferpool! = Nil { 14 // Performans nedenlerinden ötürü, yığın büyümesi meydana gelirse, morestack çağrılacaktır, 15 // Erteleme performansını daha da azaltın. Bu nedenle, yürütme için sistem yığınına geçin ve yığın büyümeyecektir. 16 sistem yığını (func { 17 kilit (sched.deferlock) 18 len için (pp.deferpool) < cap (pp.deferpool) / 2 sched.deferpool! = nil { 19 g: = sched.deferpool 20 sched.deferpool = d.link 21 d.link = sıfır 22 pp.deferpool = ekleme (pp.deferpool, d) yirmi üç } 24 kilit açma (sched.deferlock) 25}) 26} 2728 // P'den yerel olarak ata 29 eğer n: = len (pp deferpool); n > 0 { 30 d = pp defer havuzu 31 pp. Deferpool = sıfır 32 pp.deferpool = pp.deferpool 33} 34} 35 // Önbellek yok, yeni erteleme ve değiştirgeler doğrudan öbekten ayrılıyor 36 if d == nil { 37 systemstack (func { Toplam 38: = toplam boyut (toplam boyut (uintptr (boyut))) 39 d = (* _defer) (mallocgc (toplam, deferType, doğru)) 40}) 41} 42 // _defer örneğini Goroutine'in _defer listesine ekleyin. 43 d.siz = siz 44 d.heap = doğru 45 d.link = gp._defer 46 gp._defer = d 47 dönüş d 48}

Erteleme, derleyici tarafından işlevin sonuna eklenir.Ona atlandığında, ertelenmesi gereken giriş adresi çıkarılır, ardından atlanır ve çalıştırılır:

1 // src / runtime / panic.go 23 // git: nosplit 4func deferreturn (arg0 uintptr) { 5 gp: = getg 6 g: = gp._defer 7 if d == nil { 8 dönüş 9} 10 // Ertelemeyi arayanın, erteleme dönüşünü arayan kişi olup olmadığını belirleyin 11 sp: = getcallersp 12 if d.sp! = Sp { 13 dönüş 14} 15 ... 1617 // Parametreleri _defer kaydından kopyalayın 18 anahtar d.siz { 19 vaka 0: // hiçbir şey yapma 20 vaka sys.PtrSize: 21 * (* uintptr) (güvensiz.Pointer (arg0)) = * (* uintptr) (deferArgs (d)) 22 varsayılan: 23 memmove (unsafe.Pointer (arg0), deferArgs (d), uintptr (d.siz)) yirmi dört } 25 // Gecikmeli fn çağrısının giriş adresini alın ve ardından _defer'i hemen bırakın 26 fn: = d.fn 27 d.fn = nil 28 gp._defer = d.link 29 serbest gemi (d) 3031 // Ara ve bir sonraki ertelemeye atla 32 jmpdefer (fn, uintptr (güvensiz.Pointer (arg0))) 33}

Bu işlevde, erteleme parametreleri gerektiğinde tekrar kopyalanacak ve birden çok erteleme işlevi jmpdefer kuyruk çağrıları olarak uygulanacaktır. Fn'ye atlamadan önce, _defer örneği serbest bırakılır ve döndürülür. Jmpdefer'in gerçekten ihtiyaç duyduğu şey, işlevin giriş adresi ve parametreleri ile çağıran erteleme dönüşünün SP'sidir:

1 // src / runtime / asm_amd64.s 23 // func jmpdefer (fv * funcval, argp uintptr) 4TEXT çalışma zamanı · jmpdefer (SB), NOSPLIT, 0-16 ABD doları 5 MOVQ fv + 0 (FP), DX // DX = fn 6 MOVQ argp + 8 (FP), BX // arayan SP 7 LEAQ -8 (BX), SP // Çağrıdan sonra Arayan SP 8 MOVQ -8 (SP), BP // BP'yi erteleme dönüşü dönüyormuş gibi geri yükle 9 SUBQ $ 5, (SP) // tekrar CALL'a dön 10 MOVQ 0 (DX), BX // BX = DX 11 JMP BX // Ertelenen işlev en son çalıştırılır

Bu jmpdefer ile ilgili dahice olan şey, gecikmeli dönüşün giriş adresini hesaplamak için arayan SP'yi kullanmasıdır, böylece bir erteleme çağrısı tamamlandıktan sonra, erteleme işlevi geri döndüğünde yığından çıkacak ve erteleme dönüşünün ilk konumuna geri dönecektir. Erteleme yanılsamasını simüle etmek için sürekli olarak kendi kendine yinelemeli kuyruk aramaya devam edin.

Serbest bırakma işlemi çok yaygındır, basitçe onu P'nin erteleme havuzuna döndürür ve yerel havuz dolduğunda bunu global kaynak havuzuna döndürür:

1 // src / runtime / panic.go 23 // git: nosplit 4func freedefer (d * _defer) { 5 ... 6 sc: = deferclass (uintptr (d.siz)) 7 eğer sc > = uintptr (len (p {}. deferpool)) { 8 dönüş 9} 10 pp: = getg.m.p.ptr 11 // P yerel havuzu doluysa, performans nedenleriyle de kaynakların yarısını küresel havuza koyun 12 // İşlem, yürütme için sistem yığınına geçecek. 13 if len (pp.deferpool) == cap (pp.deferpool) { 14 systemstack (func { 15 değişken önce, sonda * _defer 16 len için (pp.deferpool) > kap (pp.deferpool) / 2 { 17 n: = len (pp.deferpool) 18 gün: = pp deferpool 19 pp. Deferpool = sıfır 20 pp.deferpool = pp.deferpool 21 eğer ilk == nil { 22 birinci = d 23} başka { 24 last.link = d 25} 26 son = gün 27} 28 kilit (sched.deferlock) 29 last.link = sched.deferpool 30 sched.deferpool = ilk 31 kilit açma (sched.deferlock) 32}) 33} 3435 // _defer'in sıfır değerini geri yükleyin, yani * d = _defer {} 36 d.siz = 037 ... 38 d.sp = 039 d.pc = 040 d.framepc = 041 ... 42 d.link = sıfır 4344 // P yerel kaynak havuzuna koy 45 pp.deferpool = ekleme (pp.deferpool, d) 46}

Yığın üzerinde erteleme oluşturun

Erteleme ayrıca, kayıt ertelemesinin ikinci biçimi olan deferprocStack olan yığın üzerine doğrudan tahsis edilebilir. Yığın üzerinde erteleme ayırmanın avantajı, işlev döndükten sonra _defer'in serbest bırakılmış olmasıdır ve artık bellek ayırma sırasında üretilen performans ek yükünü dikkate almaya gerek yoktur.Sadece _defer bağlantılı listeyi düzgün bir şekilde tutmanız gerekir.

SSA aşaması ile yığın ayırma arasındaki fark, yığın üzerinde erteleme oluşturmak için derleyiciyi doğrudan işlev çağrısı çerçevesinde _defer kaydını başlatmak ve bunu deferprocStack'e bir parametre olarak iletmek için kullanmanız gerektiğidir:

1 // src / cmd / derleme / dahili / gc / ssa.go 2func (s * durum) çağrı (n * Düğüm, k callKind) * ssa.Value { 3 ... 4 var çağrı * ssa.Value 5 if k == callDeferStack { 6 // Doğrudan yığın üzerinde bir erteleme kaydı oluşturun 7 t: = deferstruct (stksize) // Derleyici perspektifinden _defer yapısını inşa et 8 g: = tempAt (n.Pos, s.curfn, t) 910 s.vars = s.newValue1A (ssa.OpVarDef, types.TypeMem, d, s.mem) 11 adres: = s.addr (d, yanlış) 1213 // _defer'in her alanını yığına kaydetmek için alan ayırın 14 s. Mağaza (türler, türler, 15 s.newValue1I (ssa.OpOffPtr, types.Types.PtrTo, t.FieldOff (0), addr), 16 s.constInt32 (types.Types, int32 (stksize))) 17 s.store (closure.Type, 18 s.newValue1I (ssa.OpOffPtr, closure.Type.PtrTo, t.FieldOff (6), addr), 19 kapatma) 2021 // Erteleme çağrısına katılan fonksiyon parametrelerini kaydedin 22 ft: = fn. Tür 23 kapalı: = t.FieldOff (12) 24 bağımsız değişken: = n.Rlist.Slice 2526 // deferprocStack'i çağırın, _defer tarafından kaydedilen işaretçiyi parametre olarak iletir 27 arg0: = s.constOffPtrSP (types.Types, Ctxt.FixedFrameSize) 28 s.store (types.Types, arg0, addr) 29 call = s.newValue1A (ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem) 30 ... 31} başka {...} 3233 // Fonksiyonun sonu, yığın üzerinde ayrılan yığınla aynıdır, deferreturn çağrısı 34 if k == callDefer || k == callDeferStack { 35 ... 36 s. Çıkış 37} 38 ... 39}

Derleme aşamasında, yığın üzerinde bir _defer kaydının boşluğunun ayrıldığı ve deferprocStack'in rolünün yalnızca kaydı çalışma zamanında başlatmak olduğu görülebilir:

1 // src / runtime / panic.go 23 // git: nosplit 4func deferprocStack (d * _defer) { 5 gp: = getg 6 // siz ve fn'nin derleme aşamasında ayarlandığını ve burada yalnızca diğer alanların başlatıldığını unutmayın 7 d.started = yanlış 8 d.heap = false // Ertelemenin şu anda yığın üzerinde ayrılmamış olarak işaretlendiği görülebilir. 9 d.openDefer = yanlış 10 d.sp = getcallersp 11 d.pc = getcallerpc 12 ... 13 // Yığın üzerinde tahsis edilmiş olmasına rağmen, birden çok _defer kaydının yine de bağlantılı bir liste aracılığıyla birleştirilmesi gerekir, 14 // Gecikmeli dönüşte gecikmeli fonksiyonun giriş adresini bulmak için: 15 // d.link = gp._defer 16 // gp._defer = d 17 * (* uintptr) (güvensiz.Pointer (d.link)) = uintptr (güvensiz.Pointer (gp._defer)) 18 * (* uintptr) (güvensiz.Pointer (gp._defer)) = uintptr (güvensiz.Pointer (d)) 19 getiri020}

Fonksiyonun sonundaki davranışa gelince, bu öbek üzerinde tahsis etmek için erteleme dönüşünü çağırmakla aynıdır, bu yüzden onu tekrar etmeyeceğiz. Elbette, dahil olan freedefer çağrısının herhangi bir belleği serbest bırakması gerekmez, bu nedenle erken döner:

1 // src / runtime / panic.go 2func freedefer (d * _defer) { 3 if! D.heap {return} 4 ... 5}

Açık kodlu erteleme

Bu makalede başlangıçta açıklandığı gibi, erteleme ilk izlenimi bize aslında bir derleme zamanı özelliğidir. Daha önce defer'in neden çalışma zamanı desteğine ihtiyacı olduğunu ve ertelemenin çalışma zamanı desteğine nasıl ihtiyaç duyduğunu tartışmıştık. Şimdi, ertelemenin hangi koşullar altında yalnızca derleme zamanı özelliğine dönüşebileceğini, yani ertelenmiş işlevi doğrudan işlevin sonunda çağırarak, böylece neredeyse hiçbir ek yük olmayacak şekilde inceleyelim. Ek çalışma zamanı performansı ek yükü gerektirmeyen bu tür bir erteleme, açık kodlu bir ertelemedir. Bu tür bir erteleme ile doğrudan çağrı arasındaki performans farkı ne kadar büyük? İki performans testi de yazabiliriz:

1func çağrı {func {}} 2func callDefer {erteleme işlevi {}} 3func BenchmarkDefer (b * test.B) { İ için 4: = 0; i < b.N; i ++ { 5 call // İkinci çalıştırmada callDefer ile değiştirin 6} 7}

Go 1.14 sürümünde okuyucular, callDefer'ı kullandıktan sonra performans kaybının yaklaşık 1 ns olduğu, aşağıdakine benzer bir performans tahmini alabilir. Bu tür nanosaniye performans kaybı, bir CPU saat döngüsünden daha azdır ve açık kodlama ertelemesinin neredeyse hiç performans ek yükü olmadığını zaten düşünebiliriz:

1ame eski zaman / op yeni zaman / op delta 2 Referans-121,24ns ±% 12,23ns ±% 1 +% 80,06 (p = 0,000 n = 10 + 9)

Açık kodlama ertelemesinin son derlenmiş biçimine bir göz atalım:

1 $ go build -gcflags "-l" -ldflags = -compressdwarf = false -o main.out main.go 2 $ go aracı objdump -S main.out > main.s

Aşağıdaki formdaki işlev çağrıları için:

1var mu sync.Mutex 2func callDefer { 3 mu Kilit 4 erteleme mu. Kilidini 5}

Tüm çağrının nihai derleme sonucu ne deferproc ne deferprocStack ne de erteleme dönüşüne sahiptir. Gecikme ifadesi doğrudan işlevin sonuna eklenir:

1TEXT main.callDefer (SB) /Users/changkun/Desktop/defer/main.go 2func callDefer { 3 ... 4 mu Kilit 50x105794a 488d05071f0a00 LEAQ main.mu (SB), AX 60x105795148890424 MOVQ AX, 0 (SP) 70x1057955 e8f6f8ffff CALL sync. (* Mutex) .Lock (SB) 8 erteleme mu. Kilidini 90x105795a 488d057f110200 LEAQ go.func. * + 1064 (SB), AX 100x10579614889442418 MOVQ AX, 0x18 (SP) 110x1057966488d05eb1e0a00 LEAQ main.mu (SB), AX 120x105796d 4889442410 MOVQ AX, 0x10 (SP) 13} 140x1057972 c644240f00 MOVB $ 0x0, 0xf (SP) 150x1057977488b442410 MOVQ 0x10 (SP), AX 160x105797c 48890424 MOVQ AX, 0 (SP) 170x1057980 e8ebfbffff CALL sync. (* Mutex). Kilit açma (SB) 180x1057985488b6c2420 MOVQ 0x20 (SP), BP 190x105798a 4883c428 ADDQ $ 0x28, SP 200x105798e c3 RET yirmi bir ...

Peki açık kodlama ertelemesi nasıl gerçekleşir? Tüm ertelemeler açık kodlu mu? Hangi koşullar altında açık kodlu, çalışma zamanına bağlı bir özelliğe dejenere olur?

Üretim koşulları

İlk önce açık kodlu erteleme üretme koşullarına bakalım. SSA buildssa'nın derleme aşamasında şunlara sahibiz:

1 // src / cmd / derleme / dahili / gc / ssa.go 2const maxOpenDefers = 83func walkstmt (n * Node) * Node { 4 ... 5 switch n.Op { 6 vaka ODEFER: 7 Curfn.Func.SetHasDefer (true) 8 Curfn.Func.numDefers ++ 9 // 8 defers'dan fazla olduğunda, defers'ın açık kodlamasını devre dışı bırakın 10 If Curfn.Func.numDefers > maxOpenDefers { 11 Curfn.Func.SetOpenCodedDeferDisallowed (true) 12} 13 // Döngü deyiminde erteleme vardır ve ertelemenin açık kodlanması yasaktır. 14 // Erteleme döngü deyiminde olup olmadığı, SSA'dan önce kaçış analizinde değerlendirilecektir, 15 // Kaçış analizi bir döngü (loopDepth) olup olmadığını kontrol edecektir: 16 // if where.Op == ODEFER e.loopDepth == 1 { 17 // nerede.Esc = Asla Esc 18 // ... 19 //} 20 if n.Esc! = EscNever { 21 Curfn.Func.SetOpenCodedDeferDisallowed (true) yirmi iki } 23 vaka ... yirmi dört } 25 ... 26} 2728func buildssa (fn * Node, worker int) * ssa.Func { 29 ... 30 değişken durum 31 ... 32 s.hasdefer = fn.Func.HasDefer 33 ... 34 // Ertelemenin açık kodlanması için koşullar 35 s.hasOpenDefers = Hata Ayıkla == 0 s.hasdefer! S.curfn.Func.OpenCodedDeferDisallowed 36 eğer OpenDefers varsa 37 s.curfn.Func.numReturns * s.curfn.Func.numDefers > 15 { 38 s.hasOpenDefers = yanlış 39} 40 ... 41}

Bu şekilde, ertelemenin açık kodlamasına izin vermek için ana koşulları elde ettik (ortak üretim ortamıyla ilgili olmayan bazı koşullar burada ihmal edilmiştir, örneğin, erteleme rekabet denetimi etkinleştirildiğinde açık kodlu olamaz):

  • Derleyici optimizasyonu devre dışı bırakılmaz, yani -gcflags "-N" ayarlanmadı

  • Erteleme çağrısı var

  • Fonksiyondaki erteleme sayısı 8'i geçmez ve dönüş ifadesinin çarpımı ile geciken ifade sayısı 15'i geçmez.

  • Döngü deyiminde erteleme oluşmaz

  • Gecikme biti

    Elbette, normal yazılı erteleme derleyici tarafından doğrudan analiz edilebilir, ancak bu makalenin başında belirtildiği gibi, bir koşullu ifadede bir erteleme meydana gelirse, bu koşul çalışma zamanında belirlenmelidir:

    1 if rand.Intn (100) < 42 { 2 erteleme fmt.Println ("yaşamın anlamı") 3}

    Öyleyse, işlevin sonuna eklenen gecikmeli ifadenin koşul doğru olduğunda doğru şekilde yürütülmesini sağlamak için minimum maliyeti nasıl kullanabiliriz? Bu, gecikmiş bir ifadeye sahip bir koşullu dalın çalıştırılıp çalıştırılmadığını kaydetmek için bir mekanizma gerektirir Bu mekanizma, Go'daki erteleme bitini kullanır. Bu yaklaşım çok zekice, ancak prensip çok basit.

    Aşağıdaki kod için:

    1defer f1 (a1) 2if koşul { 3 erteleme f2 (a2) 4} 5 ...

    Gecikmeli bit kullanmanın temel fikri, aşağıdaki sözde kod ile özetlenebilir. Gecikmeli bir arama yaratma aşamasında, ilk olarak hangi koşullu ertelemenin gecikme bitinin belirli bir pozisyonu tarafından tetiklendiğini kaydedin. Bu gecikme biti, uzunluğu 8 bit olan ikili bir koddur (ayrıca donanım mimarisindeki en küçük ve en yaygın durum) Her bitin 1'e ayarlanıp ayarlanmayacağı, gecikme ifadesinin çalışma zamanında ayarlanıp ayarlanmadığını belirlemek için kullanılır. Çağrı gerçekleşti. Aksi takdirde arama yapmayın:

    1deferBits = 0 // Başlangıç değeri 000000002deferBits | = 1 < < 0 // ilk ertelemeyle karşılaş, 00000001'e ayarlanmış 3_f1 = f14_a1 = a15if koşul { 6 // İkinci erteleme ayarlandıysa, 00000011 olarak ayarlanır, aksi takdirde hala 00000001 olur 7 deferBits | = 1 < < 18 _f2 = f29 _a2 = a210}

    Çıkış konumunda, işaretli gecikme bitine göre, hangi pozisyonda tetiklenmesi gereken erteleme, gecikmeli aramayı yürütmek için geriye doğru çıkarılır:

    1çıkış: 2 // Gecikme bitlerini ters sırada kontrol edin. İkinci erteleme belirlenirse, 3 // 00000011 ve 00000010 == 00000010, yani gecikme biti sıfır değildir ve f2 çağrılmalıdır. 4 // İkinci erteleme belirlenmemişse 5 // 00000001 ve 00000010 == 00000000, yani gecikme biti sıfırdır ve f2 çağrılmamalıdır. 6if deferBits ve 1 < < 1! = 0 {// 00000011 ve 00000010! = 07 deferBits ^ = 1 < < 1 // 000000018 _f2 (_a2) 9} 10 // Benzer şekilde, 00000001 ve 00000001 == 00000001'den beri, gecikme biti sıfır değildir ve f1 çağrılmalıdır 11if ertelenirse Bit 1 < < 0! = 0 { 12 deferBits ^ = 1 < < 013 _f1 (_a1) 14}

    Gerçek uygulamada, açık kodlu bir erteleme ayarlanabildiğinde, buildssa'nın önce 8 bit uzunluğunda geçici bir değişken oluşturacağını görebilirsiniz:

    1 // src / cmd / derleme / dahili / gc / ssa.go 2func buildssa (fn * Node, worker int) * ssa.Func { 3 ... 4 if s.hasOpenDefers { 5 // deferBits geçici değişkeni oluştur 6 deferBitsTemp: = tempAt (src.NoXPos, s.curfn, types.Types) 7 s.deferBitsTemp = deferBitsTemp 8 // deferBits, 8 bitlik ikili olarak tasarlanmıştır, bu nedenle açık kodlanabilen erteleme sayısı 8'i geçemez 9 // Burada başlangıç deferBits değerini de sıfıra ayarlayın 10 startDeferBits: = s.entryNewValue0 (ssa.OpConst8, types.Types) 11 s.vars = startDeferBits 12 s.deferBitsAddr = s.addr (deferBitsTemp, false) 13 s.store (types.Types, s.deferBitsAddr, startDeferBits) 14 ... 15} 16 ... 17 s.stmtList (fn.Nbody) // s.stmt'yi çağırın 18 ... 19}

    Erteleme ifadesini kodlayın:

    1 // src / cmd / derleme / dahili / gc / ssa.go 2func (s * durum) stmt (n * Düğüm) { 3 ... 4 anahtar n.Op { 5 vaka ODEFER: 6 // Açık kodlu erteleme 7 if s.hasOpenDefers { 8 s.openDeferRecord (solda) 9} başka {...} 10 vaka ... 11} 12 ... 13} 1415 // Bulunduğu sözdizimi ağacı düğümü, gecikmeli çağrı, parametreler vb. Gibi bir erteleme çağrısı hakkındaki bilgileri depolayın. 16type openDeferInfo struct { 17 n * Düğüm 18 kapanış * ssa.Value 19 kapamaNode * Düğüm 20 ... 21 argVals * ssa.Value 22 argNodes * Düğüm yirmi üç} 24func (s * durumu) openDeferRecord (n * Node) { 25 ... 26 değişken * ssa.Value 27 var argNodes * Düğüm 2829 // Ertelemeyle ilgili giriş adresini ve parametre bilgilerini kaydedin 30 opendefer: = openDeferInfo {n: n} 31 fn: = n.Left 32 // Fonksiyon giriş adresini kaydedin 33 if n.Op == OCALLFUNC { 34 closureVal: = s.expr (fn) 35 closure: = s.openDeferSave (nil, fn.Type, closureVal) 36 opendefer.closureNode = closure.Aux. (* Düğüm) 37 if! (Fn.Op == ONAME fn.Class == PFUNC) { 38 opendefer.closure = closure 39} 40} başka { 41 ... 42} 43 // Hemen değerlendirilmesi gereken parametreleri kaydedin _ İçin 44, argn: = aralık n.Rlist.Slice { 45 var v * ssa.Value 46 eğer canSSAType (argn.Type) { 47 v = s.openDeferSave (nil, argn.Type, s.expr (argn)) 48} başka { 49 v = s.openDeferSave (argn, argn.Type, nil) 50} 51 bağımsız değişken = append (args, v) 52 argNodes = ekleme (argNodes, v.Aux. (* Düğüm)) 53} 54 opendefer.argVals = bağımsız değişken 55 opendefer.argNodes = argNodes 5657 // Her erteleme göründüğünde, len (ertelemeler) artacak ve sonra 58 // Gecikme bitleri deferBits | = 1 < < len (erteleme) farklı bitlerde ayarlanır 59 dizin: = len (s.openDefers) 60 s.openDefers = append (s.openDefers, opendefer) 61 bitvalue: = s.constInt8 (types.Types, 1 < < uint (dizin)) 62 newDeferBits: = s.newValue2 (ssa.OpOr8, types.Types, s.variable (deferBitsVar, types.Types), bitvalue) 63 s.vars = newDeferBits 64 s.store (types.Types, s.deferBitsAddr, newDeferBits) 65}

    İşlev geri dönmeden ve çıkmadan önce, durumun çıkış işlevi, gecikmiş bitler için ters sırada kontrol kodları oluşturacaktır, böylece gecikmiş işlev çağrıları sırayla çağrılır:

    1 // src / cmd / derleme / dahili / gc / ssa.go 2func (s * durumu) exit * ssa.Block { 3 if s.hasdefer { 4 if s.hasOpenDefers { 5 ... 6 s.openDeferExit 7} else { 8 ... 9} 10} 11 ... 12} 1314func (s * durumu) openDeferExit { 15 deferExit: = s.f.NewBlock (ssa.BlockPlain) 16 s.endBlock.AddEdgeTo (deferExit) 17 s.startBlock (deferExit) 18 s.lastDeferExit = deferExit 19 s.lastDeferCount = uzunluk (s.openDefers) 20 sıfırlama: = s.constInt8 (types.Types, 0) 21 // ters sıra kontrolü erteleme İ için 22: = len (s.openDefers) -1; i > = 0; i-- { 23 r: = s.openDefers 24 bCond: = s.f.NewBlock (ssa.BlockPlain) 25 bEnd: = s.f.NewBlock (ssa.BlockPlain) 2627 // deferBits'i kontrol edin 28 deferBits: = s.variable (deferBitsVar, türler.Tipler) 29 // deferBits ve 1 ise oluştur < < len (erteleme)! = 0 {...} 30 bitval: = s.constInt8 (types.Types, 1 < < uint (i)) 31 andval: = s.newValue2 (ssa.OpAnd8, types.Types, deferBits, bitval) 32 eqVal: = s.newValue2 (ssa.OpEq8, types.Types, andval, zeroval) 33 b: = s.endBlock 34 b.Kind = ssa.BlockIf 35 b. SetControl (eqVal) 36 b. Kenar Ekleme (bEnd) 37 b. Ekleme Kenarı (bCond) 38 bCond.AddEdgeTo (bEnd) 39 sn. StartBlock (bCond) 4041 // Oluşturulan koşullu dal tetiklenirse, mevcut gecikme bitlerini temizleyin: deferBits ^ = 1 < < len (geciktirir) 42 nbitval: = s.newValue1 (ssa.OpCom8, types.Types, bitval) 43 maskedval: = s.newValue2 (ssa.OpAnd8, types.Types, deferBits, nbitval) 44 s.store (types.Types, s.deferBitsAddr, maskedval) 45 s.vars = maskeli değer 4647 // Gecikmeli fonksiyon çağrısını işleyin, kaydedilen giriş adresini ve parametre bilgilerini çıkarın 48 argStart: = Ctxt.FixedFrameSize 49 fn: = r.n.Left 50 stksize: = fn.Type.ArgWidth 51 ... J için 52, argAddrVal: = aralık r.argVals { 53 f: = getParam (r.n, j) 54 pt: = türler.NewPtr (örn. Tür) 55 adres: = s.constOffPtrSP (pt, argStart + f.Offset) 56 if! CanSSAType (örn. Type) { 57 s.move (örn. Type, addr, argAddrVal) 58} başka { 59 argVal: = s.load (f.Type, argAddrVal) 60 s.storeType (örn.Type, addr, argVal, 0, false) 61} 62} 63 // çağrı 64 var çağrı * ssa.Value 65 ... 66 çağrı = s.newValue1A (ssa.OpStaticCall, types.TypeMem, fn.Sym.Linksym, s.mem) 67 call.AuxInt = stksize 68 s.vars = çağrı 69 ... 70 s. Bitiş bloğu 71 s.startBlock (bEnd) 72} 73}

    Tüm süreçten, açık kodlu ertelemenin kesinlikle sıfır maliyet olmadığını görebiliriz.Derleyici gecikmeli çağrıyı doğrudan dönüş ifadesinden önce ekleyebilse de, anlamsal değerlendirmeler nedeniyle, yığın üzerindeki gecikmeli çağrıya katılmak gerekir. Parametresi bir kez değerlendirilir; aynı zamanda, koşullu ifadede mevcut olabilecek erteleme nedeniyle, çalışma zamanında gecikmiş bir ifadenin ayarlanıp ayarlanmadığını kaydetmek için ayrıca gecikme biti gerekir. Bu nedenle, açık kodlu ertelemenin maliyeti, çalışma zamanında ertelenmesi gereken bir erteleme olup olmadığını belirlemek için çok az sayıda talimat ve bit işlemine yansıtılır.

    Defer'ın optimizasyon yolu

    Sonunda gecikmiş cümlelerin tüm evrimini gözden geçirelim.

    Ertelemenin erken uygulanması aslında çok zordu. Bir erteleme çağrısı gerçekleştiğinde, yığın üzerinde bir erteleme kaydı tahsis edilir ve çağrıda yer alan parametrelerde bir kopyalama işlemi gerçekleştirilir ve ardından erteleme listesine eklenir; işlev geri döndüğünde ve erteleme çağrısını tetiklemesi gerektiğinde, erteleme listeden sırayla kaldırılır. Çıkarın ve aramayı tamamlayın. Elbette, ilk uygulamanın mükemmel olması gerekmez ve performans sorunları gelecekte her zaman yinelenebilir.

    Go 1.1'in geliştirme aşamasında, erteleme ilk optimizasyonunu aldı. Russ Cox, erteleme performans sorunlarının temel nedeninin, birden çok erteleme çağrısı üretildiğinde aşırı bellek ayırma ve kopyalama işlemleri olduğunu fark etti ve ardından her bir Goroutine'de erteleme tahsisi ve bırakma sürecini toplu işlemeyi önerdi. O zaman, Dmitry Vyukov, yığın üzerindeki tahsisatın daha etkili olacağını öne sürdü, ancak Russ Cox, yanlışlıkla yürütme yığınındaki erteleme kayıtlarının tahsisinin ve diğer yerlerde tahsisin çok fazla fayda sağlamadığına ve sonunda G başına parti tahsisini gerçekleştirdiğine inanıyordu. Erteleme mekanizması.

    Sonraki programlayıcının iyileştirilmesi ve iş çalma planlamasının getirilmesi nedeniyle, çalışma zamanı, per-P'nin yerel kaynak havuzunu desteklemeye başladı.Goroutine'de gerçekleşen bir çağrı olarak, gerekli bellek doğal olarak yerel olarak tutulan olarak kabul edilebilecek bir tür bellektir. Kaynaklar. Bu nedenle, kaynak tahsisi ve ertelemenin serbest bırakılması Go 1.3'te optimize edilmiştir Dmitry Vyukov G başına tahsis edilen ertelemeyi P başına kaynak havuzundan tahsis edilen mekanizmaya değiştirmiştir.

    Tahsis gecikme kaydı _defer çağrısı, yerel kaynak havuzuna sahip olabileceğinden, küresel kaynak havuzunun yeniden kullanılabilir hafızası yoktur, bu da yığın bölünmesine yol açar, daha da kötüsü, M / P'nin bağlanmasının çözülmesine ve bağlanmasına yol açan ön alım meydana gelebilir Ek planlama ek yükünü bekleyin. Bu nedenle, Austin Clements tarafından ertelemenin bir optimizasyonu, her erteleme ve erteleme dönüşünde sistem yığınına geçmektir, böylece önleme ve yığın büyümesinin meydana gelmesi engellenir ve optimizasyon, ön emmenin getirdiği M / P bağlanmasını ortadan kaldırır. Tepegöz geliyor. Ek olarak, bir kayıt her oluşturulduğunda, memmove sistem çağrısı parametre boyutuna bakılmaksızın dahil edilir ve bu da bir memmove çağrısı maliyetiyle sonuçlanır.Austin'in optimizasyonu ayrıca parametresiz iki durumu ve işaretçi boyutu parametresini özellikle ele alır. Yargılamak, böylece memmove'un bu özel durumlarda neden olduğu ek yükü atlamak.

    Keith Randall Dmitry Vyukov defer Go 1.13 defer

    Go 1.14 Dan Scales Go defer defer defer Go 1.14

    defer

    özet

    defer defer

    defer

  • defer

    • defer 15 7 defer 2 defer 8 defer

    • defer

    defer

    • _defer deferprocStack _defer Goroutine

    • defer deferreturn

    • defer _defer

    defer

    • deferproc _defer Goroutine

    • defer deferreturn _defer defer

    • defer defer

    • Robert Griesemer. defer statement. Jan 27, 2009. https://github.com/golang/go/commit/4a903e0b32be5a590880ceb7379e68790602c29d

    • Ken Thompson. defer. Jan 27, 2009. https://github.com/golang/go/commit/1e1cc4eb570aa6fec645ff4faf13431847b99db8

    • Russ Cox. runtime: aggregate defer. Oct, 2011. https://github.com/golang/go/issues/2364

    • Austin Clements. runtime: optimize defer code. Sep, 2016. https://github.com/golang/go/commit/4c308188cc05d6c26f2a2eb30631f9a368aaa737

    • Minux Ma. runtime: defer is slow. Mar, 2016. https://github.com/golang/go/issues/14939

    • Keith Randall. cmd/compile: allocate some defers in stack frames. Dec, 2013. https://github.com/golang/go/issues/6980

    • Dmitry Vyukov. runtime: per-P defer pool. Jan, 2014. https://github.com/golang/go/commit/1ba04c171a3c3a1ea0e5157e8340b606ec9d8949

    • Dan Scales, Keith Randall, and Austin Clements. Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case. Sep, 2019. https://go.googlesource.com/proposal/+/refs/heads/master/design/34481-opencoded-defers.md

    Kodsuz çağın gelişiyle, programcılar işlerini nasıl sürdürebilirler?
    önceki
    GitHub'ın ortadaki bir adam tarafından saldırıya uğradığından şüpheleniliyor ve en büyük karanlık web sunucusu tekrar saldırıya uğradı
    Sonraki
    Huawei P40 "bir çocuk ve üç çocuk", en pahalı fiyat 10854 yuan
    Wechat, "dağıtım" işlevini küçük bir aralıkta başlattı; Luo Yonghao, Douyin ile özel bir sözleşme yaptığını duyurdu; Github sayfaları, ortadaki adam saldırısıyla karşılaşabilir | Geek Headlines
    Java'da kendi Kubernetes denetleyicinizi geliştirin, denemek ister misiniz?
    On milyonlarca ölçek kategorisi sınıflandırma teknolojisini destekleyen Baidu Feida, endüstriyel düzeyde bir derin öğrenme çerçevesi tanımlıyor
    Biz programcılar yazılım geliştirmeyi neden ciddiye almıyoruz?
    Dürüstlük ve saygı: Profesyonel güçlerle salgınla mücadeleye yardımcı olmak
    Kar yağıyor! Liaocheng'in Guanxian İlçesindeki on bin dönüm armut çiçeği tarlaları kaplayarak tamamen çiçek açmış durumda (Fotoğraflar)
    Qingdao Havaalanındaki tıbbi araştırma kabinini 48 saat içinde inşa edin! Çin İnşaat Sekizinci Bürosu Dördüncü Şirket Demir Ordu Savaş Salgını
    Hava sınıfı "Binzhou Modu"! 150 milyon sayfa görüntülemenin arkasında bir dizi terleme sayısı var
    Dün gece "çizin"! 10 milyon! ilk! Jackpot burada ...
    İşe yeniden başladıktan sonra, yavaş yavaş "toparlandı" ve Xiangtan'daki iki kitapçı ve kitap pazarında dolaştı.
    Ulaştırma Bakanlığı: Ulaştırma ekonomik operasyonu Ocak ayından Şubat ayına kadar daha büyük bir düşüş baskısıyla karşılaşacak
    To Top