回覆列表
  • 1 # 東方瑞通

    處理特定問題的方式是Julia和Python之間的一個關鍵區別,而Julia的構建是為了減輕高效能計算的挑戰。雖然Python已經發展為一種快速的計算語言,但是它不是為這項工作而設計的。而Julia相對於Python則更具專業性,在高速處理和計算工作中。

    不久前,Julia釋出了一個穩定的1.2版本,有了進一步的改進,可以更加高速地處理佔用大量資源的資料科學專案等。

  • 2 # 智商249

    julia可以呼叫很多Python的庫,就是假裝說在Python與c/c++之間找到了哥平衡點。但是寫多了感覺語言吧只要掌握一種就好,換個語言就是換種寫法,很快就能掌握。最主要的區別就是,python的Numpy的array只能在cpu上執行,無法用用gpu加速。julia支援GPU加速

  • 3 # AI智慧

    Julia和Python的關鍵區別

    你知道為什麼 Julia 的速度能做到那麼快嗎?這並不是因為更好的編譯器,而是一種更新的設計理念,Julia 在開發之初就將這種理念納入其中,而這也是關注 “人生苦短” 的 Python 所欠缺的。

    短短几年,由 MIT CSAIL 實驗室開發的程式語言 Julia 已然成為程式設計界的新寵,尤其在科學計算領域炙手可熱。很大部分是因為這門語言結合了 C 語言的速度、Ruby 的靈活、Python 的通用性,以及其他各種語言的優勢於一身。

    Julia因為它比其他指令碼語言更快,它在具備 Python、MATLAB、R 語言開發速度的同時,又能生成與 C 語言和 Fortran 一樣快的程式碼。

    文末有關於Julia速度的測試和Python的關鍵區別結論!

    Python市場

    全世界有超過800萬的開發人員出於各種目的熱忠於使用Python。由於其動態特性和易於擴充套件性,Python已經成為開發人員的首選語言。這也是為什麼Python能夠擊敗Java的原因,Java一度以來都是開發人員最喜歡的語言。也可能是由於一門語言的自然老化過程,Java正在接近尾聲。大多數新語言都是為解決現代面臨的新挑戰而設計的。雖然之前開發的語言在解決當時的問題時效率極高,但要讓它們跟上不斷變化的行業和市場就變得極其困難。

    但是,Python作為一種擁有如此龐大使用者和開發者支援的開源語言,即使在今天仍然保持著它的巔峰狀態。它豐富的庫和內建的功能使其成為企業、開發人員和資料科學家的熱門選擇。儘管Java仍然被用於企業開發,但它在其他領域的相關性幾乎為零。如果環顧四周,你很難發現一個機器學習專家在Java上設計和訓練模型。儘管如此,Java是全球第二大最受開發人員歡迎的語言。

    取代Java

    Python已經成功地在大多數領域取代了Java。在企業開發方面,Java面臨著來自谷歌的新程式語言Go的威脅。隨著我們進入未來科技時代,對高效能計算的需求也在不斷增長。這也是資料科學和人工智慧的時代需求。儘管有人可能認為使用extreme GPU有助於提高速度和效率,但事實遠非如此。它不能滿足特定的資料處理需求。相反,前沿應用程式需要其他依賴項來最佳化效能,並幫助科學家和開發人員實現預期的目標。最終,這將引導企業和研究機構尋找更健壯的程式語言,為特定的任務及其交付速度而設計。

    進入Julia的世界

    這個人人都喜愛Python的時代,正面臨著來自程式語言世界的新參與者——Julia的威脅。Viral Shah是Julia Computing的執行長,他指出,在21世紀初,開發人員更喜歡用C語言進行系統程式設計,用JAVA開發企業應用程式,用SaaS進行分析,用MATLAB進行科學計算。然而,今天的開發人員使用Rust進行系統程式設計,Go進行企業開發,使用Python/R進行分析,並使用Julia進行科學計算。

    Julia立足之地

    Julia和Python之間的一個關鍵區別是處理特定問題的方式。Julia的構建是為了減輕高效能計算的挑戰。儘管Python現在已經發展為一種快速的計算語言,但是我們必須承認它不是為這項工作而設計的。然而,Julia是專門為高速處理和計算工作設計的。雖然它只有幾個月的歷史,卻已經在研究人員和資料科學家中引起轟動。

    目前有超過800名Julia開發人員,他們正在為GitHub做貢獻,幫助其成為首選語言。

    Julia基準測試來證明它的速度

    這似乎有違 “天底下沒有免費的午餐” 的道理。它真的有那麼完美嗎?

    很多人認為 Julia 執行速度很快,因為它是即時編譯(JIT)型的(也就是說,每條語句都使用編譯的函式來執行,這些函式要麼在使用之前進行即時編譯,要麼在之前已經編譯過並放在快取中)。這就引出了一個問題:Julia 是否提供了比 Python 或 R 語言(MATLAB 預設使用 JIT)更好的 JIT 實現?因為人們在這些 JIT 編譯器上所做的工作比 Julia 要多得多,所以我們憑什麼認為 Julia 這麼快就會超過這些編譯器?但其實這完全是對 Julia 的誤解。

    我想以一種非常直觀的方式說明,Julia 的速度之所以快,是因為它的設計決策。

    Julia 的的核心設計決策是透過多重分派實現專門化的型別穩定性,編譯器因此可以很容易地生成高效的程式碼,同時還能夠保持程式碼的簡潔,讓它 “看起來就像一門指令碼語言”。

    但是,在本文的示例中,我們將看到 Julia 並不總是像其他指令碼語言那樣,我們必須接受 “午餐不全是免費” 的事實。

    要看出它們之間的區別,我們只需要看看基本的數學運算。

    Julia 中的數學運算

    一般來說,Julia 中的數學運算與其他指令碼語言中的數學運算看起來是一樣的。它們的數字都是 “真正的數字”,比如 Float64 就是 64 位浮點數或者類似於 C 語言中的 “double”。Vector {Float64} 與 C 語言 double 陣列的記憶體佈局是一樣的,都可以很容易地與 C 語言進行互操作(實際上,在某種意義上,“Julia 是構建在 C 語言之上的一個層”),從而帶來更高的效能。

    使用 Julia 進行一些數學運算:

    a = 2+ 2

    b = a/ 3

    c = a÷ 3#div tab completion, means integer division

    d = 4* 5

    println([a;b;c;d])

    [ 4.0, 1.33333, 1.0, 20.0]

    我在這裡使用了 Julia 的 unicode 製表符補全功能。Julia 允許使用 unicode 字元,這些字元可以透過製表符實現 Latex 風格的語句。同樣,如果一個數字後面跟著一個變數,那麼不需要使用 * 運算子就可以進行乘法運算。例如,下面的 Julia 的程式碼是合法的:

    α = 0.5

    ∇f(u) = α*u; ∇f( 2)

    sin( 2π)

    -2.4492935982947064e-16

    型別穩定性和程式碼內省

    型別穩定性是指一個方法只能輸出一種可能的型別。

    例如:*(::Float64,::Float64) 輸出的型別是 Float64。不管你給它提供什麼引數,它都會返回一個 Float64。這裡使用了多重分派:“*” 運算子根據它看到的型別呼叫不同的方法。例如,當它看到浮點數時,就會返回浮點數。Julia 提供了程式碼自省宏,可以看到程式碼被編譯成什麼東西。因此,Julia 不只是一門普通的指令碼語言,還是一門可以讓你處理彙編的指令碼語言!和其他很多語言一樣,Julia 被編譯成 LLVM (LLVM 是一種可移植的彙編格式)。

    @code_llvm 2* 5

    ; Function*

    ; Location: int.jl: 54

    define i64 @ "julia_*_33751"(i64, i64) {

    top:

    % 2= mul i64 % 1, % 0

    ret i64 % 2

    }

    這段程式碼的意思是:執行一個浮點數乘法操作,然後返回結果。我們也可以看一下彙編程式碼。

    @code_native2* 5

    .text

    ; Function* {

    ; Location: int. jl: 54

    imulq %rsi, %rdi

    movq %rdi, %rax

    retq

    nopl (%rax,%rax)

    ;}

    “*” 函式被編譯成與 C 語言或 Fortran 中完全相同的操作,這意味著它可以達到相同的效能(儘管它是在 Julia 中定義的)。因此,Julia 不僅可以 “接近” C 語言,而且實際上可以得到相同的 C 語言程式碼。那麼在什麼情況下會發生這種情況?

    Julia 的有趣之處在於,上面的這個問題其實問得不對,正確的問題應該是:在什麼情況下程式碼不能被編譯成像 C 語言或 Fortran 那樣?這裡的關鍵是型別穩定性。如果一個函式是型別穩定的,那麼編譯器就會知道函式在任意時刻的型別,就可以巧妙地將其最佳化為與 C 語言或 Fortran 相同的彙編程式碼。如果它不是型別穩定的,Julia 必須進行昂貴的 “裝箱”,以確保在操作之前知道函式的型別是什麼。

    這是 Julia 與其他指令碼語言之間最關鍵的不同點。

    好的方面是 Julia 的函式(型別穩定)基本上就是 C 語言或 Fortran 的函式,因此 “^”(乘方)運算速度很快。那麼,型別穩定的 ^(::Int64,::Int64) 會輸出什麼?

    2^5

    32

    2^ -5

    0 .03125

    這裡我們會得到一個錯誤。為了確保編譯器可以為 “^” 返回一個 Int64,它必須丟擲一個錯誤。但在 MATLAB、Python 或 R 語言中這麼做是不會丟擲錯誤的,因為這些語言沒有所謂的型別穩定性。

    如果沒有型別安全性會怎樣?讓我們看一下程式碼:

    @code_native ^( 2, 5)

    .text

    ; Function ^ {

    ; Location: intfuncs.jl: 220

    pushq %rax

    movabsq $power_by_squaring, %rax

    callq *%rax

    popq %rcx

    retq

    nop

    ;}

    現在,我們來定義自己的整數乘方運算。與其他指令碼語言一樣,我們讓它變得更 “安全”:

    functionexpo(x,y)

    ify> 0

    returnx^ y

    else

    x= convert(Float64, x)

    returnx^ y

    end

    end

    expo (generic functionwith1 method)

    現在執行一下看看行不行:

    println(expo( 2, 5))

    expo( 2,- 5)

    32

    0.03125

    再來看看彙編程式碼。

    @code_native expo( 2, 5)

    .text

    ; Functionexpo{

    ; Location: In[ 8]: 2

    pushq %rbx

    movq %rdi, %rbx

    ; Function>; {

    ; Location: operators.jl: 286

    ; Function<; {

    ; Location: int.jl: 49

    testq %rdx, %rdx

    ;}}

    jle L36

    ; Location: In[ 8]: 3

    ; Function^; {

    ; Location: intfuncs.jl: 220

    movabsq $power_by_squaring, %rax

    movq %rsi, %rdi

    movq %rdx, %rsi

    callq *%rax

    ;}

    movq %rax, (%rbx)

    movb $ 2, %dl

    xorl %eax, %eax

    popq %rbx

    retq

    ; Location: In[ 8]: 5

    ; Functionconvert; {

    ; Location: number.jl: 7

    ; FunctionType; {

    ; Location: float.jl: 60

    L36:

    vcvtsi2sdq %rsi, %xmm0, %xmm0

    ;}}

    ; Location: In[ 8]: 6

    ; Function^; {

    ; Location: math.jl: 780

    ; FunctionType; {

    ; Location: float.jl: 60

    vcvtsi2sdq %rdx, %xmm1, %xmm1

    movabsq $__pow, %rax

    ;}

    callq *%rax

    ;}

    vmovsd %xmm0, (%rbx)

    movb $ 1, %dl

    xorl %eax, %eax

    ; Location: In[ 8]: 3

    popq %rbx

    retq

    nopw %cs:(%rax,%rax)

    ;}

    這是一個非常直觀的演示,說明了 Julia 透過使用型別推斷獲得了比其他指令碼語言更高的效能。

    核心思想:多重分派 + 型別穩定性 => 速度 + 可讀性

    型別穩定性是 Julia 區別於其他指令碼語言的一個關鍵特性。

    事實上,Julia 的核心思想是這樣的:

    多重分派允許一種語言將函式呼叫分派給型別穩定的函式。

    這就是Julia 的核心思想,現在讓我們花點時間深入瞭解一下。

    如果函式內部具有型別穩定性(也就是說,函式內的任意函式呼叫也是型別穩定的),那麼編譯器就會知道每一步的變數型別,它就可以在編譯函式時進行充分的最佳化,這樣得到的程式碼基本上與 C 語言或 Fortran 相同。多重分派在這裡可以起到作用,它意味著 “*” 可以是一個型別穩定的函式:對於不同的輸入,它有不同的含義。但是,如果編譯器在呼叫 “*” 之前能夠知道 a 和 b 的型別,那麼它就知道應該使用哪個 “*” 方法,這樣它就知道 c=a*b 的輸出型別是什麼。這樣它就可以將型別資訊一路傳下去,從而實現全面的最佳化。

    我們從中可以學到一些東西。首先,為了實現這種級別的最佳化,必須具有型別穩定性。大多數語言為了讓使用者可以更輕鬆地編碼,都沒有在標準庫中提供這種特性。其次,需要透過多重分派來專門化型別函式,讓指令碼語言語法 “看上去更顯式” 一些。最後,需要一個健壯的型別系統。為了構建非型別穩定的乘方運算,我們需要使用轉換函式。因此,要在保持指令碼語言的語法和易用性的同時實現這種原始效能必須將語言設計成具有多重分派型別穩定性的語言,並提供一個健壯的型別系統。

    Julia 基準測試

    Julia 官網提供的基準測試只是針對程式語言元件的執行速度,並沒有說是在測試最快的實現,所以這裡存在一個很大的誤解。R 語言程式設計師一邊看著使用 R 語言實現的 Fibonacci 函式,一邊說:“這是一段很糟糕的程式碼,不應該在 R 語言中使用遞迴,因為遞迴很慢”。但實際上,Fibonacci 函式是用來測試遞迴的,而不是用來測試語言的執行速度的。

    Julia 使用了型別穩定函式的多重分派機制,因此,即使是早期版本的 Julia 也可以最佳化得像 C 語言或 Fortran 那樣。非常明顯,幾乎在所有情況下,Julia 都非常接近 C 語言。當然,也有與 C 語言不一樣的地方,我們可以來看看這些細節。首先是在計算 Fibonacci 數列時 C 語言比 Julia 快 2.11 倍,這是因為這是針對遞迴的測試,而 Julia 並沒有完全為遞迴進行過最佳化。Julia 其實也可以加入這種最佳化(尾遞迴最佳化),只是出於某些原因他們才沒有這麼做,最主要是因為:可以使用尾遞迴的地方也可以使用迴圈,而迴圈是一種更加健壯的最佳化,所以他們建議使用迴圈來代替脆弱的尾遞迴。

    Julia 表現不太好的地方還有 rand_mat_stat 和 parse_int 測試。這主要是因為邊界檢查導致的。在大多數指令碼語言中,如果你試圖訪問超出陣列邊界的元素就會出錯,Julia 預設情況下也會這麼做。

    function test1()

    a = zeros(3)

    for i=1:4

    a[i] = i

    end

    end

    test1()

    BoundsError: attempt toaccess3- elementArray{Float64, 1} atindex[ 4]

    Stacktrace:

    [ 1] setindex! at./array.jl: 769[inlined]

    [ 2] test1() at./ In[ 11]: 4

    [ 3] top- levelscopeatIn[ 11]: 7

    不過,你可以使用 @inbounds 宏來禁用這個功能:

    functiontest2()

    a= zeros(3)

    @inboundsfori=1:4

    a[i] = i

    end

    end

    test2()

    這樣你就獲得了與 C 語言或 Fortran 一樣的不安全行為和執行速度。這是 Julia 的另一個有趣的特性:預設情況下是一個安全的指令碼語言特性,在必要的時候禁用這個功能,以便獲得性能提升。

    嚴格型別

    除了型別穩定性,你還需要嚴格型別。在 Python 中,你可以將任何東西放入陣列中。而在 Julia 中,你只能將型別 T 放入 Vector {T} 中。Julia 提供了各種非嚴格的型別,例如 Any。如果有必要,可以建立 Vector {Any},例如:

    a = Vector{Any}( undef, 3)

    a[ 1] = 1.0

    a[ 2] = "hi!"

    a[ 3] = :Symbolic

    a

    3-element Array{Any, 1}:

    1.0

    "hi!"

    :Symbolic

    Union 是另一個不那麼極端的抽象型別,例如:

    a = Vector{Union{Float64,Int}}(undef, 3)

    a[ 1] = 1.0

    a[ 2] = 3

    a[ 3] = 1/ 4

    a

    3-element Array{Union{Float64, Int64}, 1}:

    1.0

    3

    0.25

    這個 Union 只接受浮點數和整數。不過,它仍然是一個抽象型別。接受抽象型別作為引數的函式無法知道元素的型別(在這個例子中,元素要麼是浮點數,要麼是整數),這個時候,多重分派最佳化在這裡起不到作用,所以 Julia 此時的效能就不如其他指令碼語言。

    所以我們可以得出一個性能原則:儘可能使用嚴格型別。使用嚴格型別還有其他好處:嚴格型別的 Vector {Float64} 實際上與 C 語言或 Fortran 是位元組相容的,所以不經過轉換就可以直接用在 C 語言或 Fortran 程式中。

    不免費的午餐

    很明顯,Julia 為了在保持指令碼語言特徵的同時實現效能目標,做出了非常明智的設計決策。但是,它也為此付出了一些代價。接下來,我將展示 Julia 的一些奇特的東西及其相應的工具。

    效能是可選的

    之前已經說明了 Julia 提供了多種方法來提升效能(比如 @inbounds),但我們不一定要使用它們。你也可以編寫型別不穩定的函式,雖然與 MATLAB、R 語言、Python 一樣慢,但你絕對可以這麼做。在對效能要求沒有那麼高的地方,可以將其作為一個可選項。

    檢查型別穩定性

    由於型別穩定性非常重要,Julia 為我們提供了一些工具,用來檢查一個函式是不是型別穩定的,其中最重要的是 @code_warntype 宏。讓我們用它來檢查一個型別穩定的函式:

    @code_warntype 2^ 5

    Body::Int64

    │ 2201─ % 1= invoke Base.power_by_squaring( _2::Int64, _3::Int64) ::Int64

    │ └── return% 1

    請注意,它將函式中所有變數都顯示為嚴格型別。那麼 expo 會是怎樣的?

    @code_warntype expo( 2, 5)

    Body::Union{Float64, Int64}

    │╻╷ > 21─ % 1= (Base.slt_int)( 0, y) ::Bool

    │ └── goto #3 if not %1

    │ 32─ % 3= π (x, Int64)

    │╻ ^ │ % 4= invoke Base.power_by_squaring(% 3::Int64, _3::Int64) ::Int64

    │ └── return% 4

    │ 53─ % 6= π (x, Int64)

    ││╻ Type │ % 7= (Base.sitofp)(Float64, % 6) ::Float64

    │ 6│ % 8= π (% 7, Float64)

    │╻ ^ │ % 9= (Base.sitofp)(Float64, y) ::Float64

    ││ │ % 10= $(Expr( :foreigncall, "llvm.pow.f64", Float64, svec(Float64, Float64), :( :llvmcall), 2, :(% 8), :(% 9), :(% 9), :(% 8))) ::Float64

    │ └── return% 10

    請注意,可能的返回值是%4 和%10,它們是不同的型別,因此返回型別被推斷為 Union {Float64,Int64}。為了準確地追蹤這種不穩定性發生的位置,我們可以使用 Traceur.jl:

    using Traceur

    @trace expo( 2, 5)

    ┌ Warnin g:xisassigned asInt64

    └ @ In[ 8]: 2

    ┌ Warnin g:xisassigned asFloat64

    └ @ In[ 8]: 5

    ┌ Warnin g:expo returns Union{Float64, Int64}

    └ @ In[ 8]: 2

    32

    在第 2 行,x 被分配了一個 Int,而在第 5 行又被分配了一個 Float64,因此它被推斷為 Union {Float64, Int64}。第 5 行是我們放置顯式轉換呼叫的地方,這樣我們就確定了問題所在的位置。

    處理必要的型別不穩定性

    首先,我已經證明了某些在 Julia 會出錯的函式在其他指令碼語言中卻可以 “讀懂你的想法”。在很多情況下,你會發現你可以從一開始就使用不同的型別,以此來實現型別穩定性(為什麼不直接使用 2.0^-5?)。但是,在某些情況下,你找不到合適的型別。這個問題可以透過轉換來解決,但這樣會失去型別穩定性。你必須重新考慮你的設計,並巧妙地使用多重分派。

    假設我們有一個 Vector {Union {Float64,Int}} 型別的 a,並且可能遇到必須使用 a 的情況,需要在 a 的每個元素上執行大量操作。在這種情況下,知道給定元素的型別將帶來效能的大幅提升,但由於型別位於 Vector {Union {Float64,Int}} 中,因此無法在下面這樣的函式中識別出型別:

    functionfoo(array)

    fori in eachindex(array)

    val = array[i]

    # doalgorithm Xonval

    end

    end

    foo (generic functionwith1 method)

    不過,我們可以透過多重分派來解決這個問題。我們可以在元素上使用分派:

    functioninner_foo(val)

    # DoalgorithmXonval

    end

    inner_foo(generic functionwith1 method)

    然後將 foo 定義為:

    functionfoo2(array::Array)

    foriineachindex(array)

    inner_foo(array[i])

    end

    end

    foo2(generic functionwith1 method)

    因為需要為分派檢查型別,所以 inner_foo 函式是嚴格型別化的。因此,如果 inner_foo 是型別穩定的,那麼就可以透過專門化 inner_foo 來提高效能。這就導致了一個通用的設計原則:在處理奇怪或非嚴格的型別時,可以使用一個外部函式來處理邏輯型別,同時使用一個內部函式來處理計算任務,實現最佳的效能,同時仍然具備指令碼語言的通用能力。

    REPL 的全域性作用域效能很糟糕

    Julia 全域性作用域的效能很糟糕。官方的效能指南建議不要使用全域性作用域。然而,新手可能會意識不到 REPL 其實就是全域性作用域。為什麼?首先,Julia 是有巢狀作用域的。例如,如果函式內部有函式,那麼內部函式就可以訪問外部函式的所有變數。

    functiontest(x)

    y= x+2

    functiontest2()

    y+3

    end

    test2()

    end

    test(generic functionwith1 method)

    在 test2 中,y 是已知的,因為它是在 test 中定義的。如果 y 是型別穩定的,那麼所有這些工作就可以帶來效能的提升,因為 test2 可以假設 y 是一個整數。現在讓我們來看一下在全域性作用域裡會發生什麼:

    a= 3

    functionbadidea()

    a+ 2

    end

    a= 3.0

    3.0

    因為沒有使用分派來專門化 badidea,並且可以隨時更改 a 的型別,因此 badidea 在編譯時無法進行最佳化,因為在編譯期間 a 的型別是未知的。但是,Julia 允許我們宣告常量:

    consta_cons = 3

    functionbadidea()

    a_cons+ 2

    end

    badidea(generic functionwith1 method)

    請注意,函式將使用常量的值來進行專門化,因此它們在設定後應該保持不變。

    在進行基準測試時會出現這種情況。新手會像下面這樣對 Julia 進行基準測試:

    a= 3.0

    @time fori = 1: 4

    globala

    a+= i

    end

    0.000006seconds ( 4allocation s:64bytes)

    但是,如果我們將它放在一個函式中,就可以實現最佳化。

    function timetest()

    a = 3.0

    @time fori = 1:4

    a += i

    end

    end

    timetest() # First time compiles

    timetest()

    0. 000001seconds

    0. 000000seconds

    這個問題非常容易解決:不要在 REPL 的全域性作用域內進行基準測試或計算執行時間。始終將程式碼放在函式中,或將它們宣告為 const。

    結論

    速度是 Julia 的設計目標。型別穩定性和多重分派對 Julia 編譯的專門化起到了關鍵的作用。而要達到如此精細的型別處理水平,以便儘可能有效地實現型別穩定性,並在不完全可能的情況下實現效能最佳化,需要一個健壯的型別系統。

    儘管現在很難說它能否完全接管Python,但它設計用於處理複雜的計算特性肯定會對世界產生影響。此外,隨著問題的處理需要更多的資源和更高效能的計算,Julia可能會成為每個人的最愛。除非Python想要和Java一樣的命運,否則它將不得不提高其速度和效率,並不斷最佳化它的庫。它可能不只是啟動新的更新,而是完全轉換引擎,使其成為更友好的CPU語言。Python相對於Julia的一個優勢是其豐富的庫。由於Julia還處於起步階段,所以它需要很長時間才能構建像Python這樣高效、動態的庫和函式。這兩種語言之間的鬥爭才剛剛開始,但對於需要快速高效工具來實現目標的研究人員和科學家來說,Julia已經變成了一種優勢。

  • 中秋節和大豐收的關聯?
  • 東歐的烏克蘭極具大國潛質,為何一副好牌打的稀碎?