Scala vs java, hiệu suất và bộ nhớ? [đóng cửa]


160

Tôi rất muốn tìm hiểu về Scala và có một câu hỏi cơ bản mà tôi dường như không thể tìm thấy câu trả lời: nói chung, có sự khác biệt về hiệu năng và cách sử dụng bộ nhớ giữa Scala và Java không?


3
Tôi đã nghe tuyên bố rằng hiệu suất có thể rất gần. Tôi nghi ngờ nó phụ thuộc rất nhiều vào những gì bạn đang làm. (vì nó dành cho Java vs C)
Peter Lawrey

Câu trả lời cho các loại câu hỏi này là "nó phụ thuộc" - đối với hầu như mọi so sánh giữa hệ thống X và hệ thống Y. Plus, đây là bản sao của stackoverflow.com/questions/2479819/ Lỗi
James Moore

Câu trả lời:


261

Scala làm cho nó rất dễ dàng để sử dụng số lượng lớn bộ nhớ mà không nhận ra nó. Điều này thường rất mạnh mẽ, nhưng đôi khi có thể gây phiền nhiễu. Ví dụ: giả sử bạn có một chuỗi các chuỗi (được gọi array) và ánh xạ từ các chuỗi đó sang các tệp (được gọi mapping). Giả sử bạn muốn lấy tất cả các tệp trong bản đồ và đến từ các chuỗi có độ dài lớn hơn hai. Trong Java, bạn có thể

int n = 0;
for (String s: array) {
  if (s.length > 2 && mapping.containsKey(s)) n++;
}
String[] bigEnough = new String[n];
n = 0;
for (String s: array) {
  if (s.length <= 2) continue;
  bigEnough[n++] = map.get(s);
}

Phù! Công việc khó khăn. Trong Scala, cách nhỏ gọn nhất để làm điều tương tự là:

val bigEnough = array.filter(_.length > 2).flatMap(mapping.get)

Dễ dàng! Nhưng, trừ khi bạn khá quen thuộc với cách các bộ sưu tập hoạt động, điều bạn có thể không nhận ra là cách làm này đã tạo ra một mảng trung gian bổ sung (với filter) và một đối tượng bổ sung cho mọi phần tử của mảng ( mapping.gettrả về một lựa chọn). Nó cũng tạo ra hai đối tượng hàm (một cho bộ lọc và một cho bản đồ phẳng), mặc dù đó hiếm khi là một vấn đề lớn vì các đối tượng chức năng là nhỏ.

Về cơ bản, việc sử dụng bộ nhớ ở mức độ nguyên thủy là như nhau. Nhưng các thư viện của Scala có nhiều phương thức mạnh mẽ cho phép bạn tạo ra số lượng lớn các đối tượng (thường là ngắn) rất dễ dàng. Trình thu gom rác thường khá tốt với loại rác đó, nhưng nếu bạn hoàn toàn không biết bộ nhớ nào đang được sử dụng, có lẽ bạn sẽ gặp rắc rối sớm hơn ở Scala so với Java.

Lưu ý rằng mã Scala Game Benchmark Game Scala được viết theo kiểu khá giống Java để có được hiệu năng giống như Java và do đó có cách sử dụng bộ nhớ giống như Java. Bạn có thể làm điều này trong Scala: nếu bạn viết mã của mình trông giống như mã Java hiệu suất cao, thì đó sẽ là mã Scala hiệu suất cao. (Bạn thể viết nó theo phong cách Scala độc đáo hơn và vẫn có hiệu suất tốt, nhưng nó phụ thuộc vào chi tiết cụ thể.)

Tôi nên thêm rằng mỗi lần dành thời gian cho lập trình, mã Scala của tôi thường nhanh hơn mã Java của tôi vì trong Scala tôi có thể thực hiện các phần không quan trọng về hiệu năng được thực hiện với ít nỗ lực hơn và dành nhiều sự chú ý của tôi để tối ưu hóa các thuật toán và mã cho các phần quan trọng hiệu suất.


172
+1 cho đoạn cuối cùng đó. Đó là một điểm quan trọng những gì còn lại ra xem xét xa quá thường xuyên.
Kevin Wright

2
Tôi nghĩ rằng quan điểm có thể giúp rất nhiều với các vấn đề bạn đề cập. Hoặc không đúng với mảng, cụ thể?
Nicolas Payette

1
@Kevin Wright - "Đó là một điểm quan trọng không được xem xét quá thường xuyên" - Đó là điều dễ nói và khó thể hiện, và cho chúng tôi biết một số điều về kỹ năng của Rex Kerr không phải là những gì người khác kém kỹ năng đạt được.
igouy

1
>> theo phong cách thành ngữ với hiệu suất siêu hạng << Có chỗ trong trò chơi điểm chuẩn cho các chương trình Scala thành ngữ không đạt được hiệu suất "siêu hạng".
igouy

2
@RexKerr - không phải ví dụ Java của bạn tra cứu khóa ánh xạ hai lần cho mỗi Chuỗi có thể trong đó ví dụ Scala của bạn chỉ thực hiện một lần sau khi Chuỗi được chọn? Tức là họ được tối ưu hóa theo những cách khác nhau cho các tập dữ liệu khác nhau?
Seth

103

Tôi là người dùng mới, vì vậy tôi không thể thêm nhận xét vào câu trả lời của Rex Kerr ở trên (cho phép người dùng mới "trả lời" nhưng không "bình luận" là một quy tắc rất kỳ lạ btw).

Tôi đã đăng ký chỉ đơn giản là để trả lời "phew, Java thật dài dòng và chăm chỉ như vậy" trong câu trả lời phổ biến của Rex ở trên. Trong khi bạn tất nhiên có thể viết mã Scala ngắn gọn hơn, ví dụ Java đưa ra rõ ràng là đầy đủ. Hầu hết các nhà phát triển Java sẽ viết mã như thế này:

List<String> bigEnough = new ArrayList<String>();
for(String s : array) {
  if(s.length() > 2 && mapping.get(s) != null) {
    bigEnough.add(mapping.get(s));
  }
}

Và tất nhiên, nếu chúng ta sẽ giả vờ rằng Eclipse không thực hiện hầu hết việc gõ thực tế cho bạn và mọi ký tự được lưu thực sự làm cho bạn trở thành một lập trình viên tốt hơn, thì bạn có thể viết mã này:

List b=new ArrayList();
for(String s:array)
  if(s.length()>2 && mapping.get(s) != null) b.add(mapping.get(s));

Bây giờ tôi không chỉ tiết kiệm thời gian để tôi nhập tên biến đầy đủ và dấu ngoặc nhọn (giải phóng tôi để dành thêm 5 giây để suy nghĩ thuật toán sâu sắc), mà tôi còn có thể nhập mã của mình vào các cuộc thi giấu giếm và có khả năng kiếm thêm tiền những ngày lễ


7
Tại sao bạn không phải là thành viên của câu lạc bộ "ngôn ngữ hông của tháng"? Bình luận tốt đẹp. Tôi đặc biệt thích đọc đoạn cuối.
stepanian

21
Tuyệt vời đặt! Tôi trở nên mệt mỏi với các ví dụ giả định, theo đó mã Java bị thổi phồng được theo sau bởi một số ví dụ ngắn gọn được xây dựng cẩn thận của Scala (hoặc một số ngôn ngữ FP khác) và sau đó rút ra một kết luận vội vàng rằng Scala phải tốt hơn Java vì nó. Ai đã từng viết bất cứ điều gì có ý nghĩa trong Scala nào! ;-) Và đừng nói Twitter ...
chrisjleu

2
Chà, giải pháp của Rex sẽ phân bổ bộ nhớ cho mảng, điều này sẽ làm cho mã được biên dịch chạy nhanh hơn (vì với cách tiếp cận của bạn, bạn hãy để JVM định kỳ phân bổ lại mảng của bạn khi nó phát triển). Mặc dù có nhiều thao tác đánh máy hơn, nhưng hiệu suất khôn ngoan có thể là một người chiến thắng.
Ashalynd

5
trong khi chúng tôi ở đó, trong java8 sẽ là:Arrays.stream(array).map(mapping::get).filter(x->x!=null).toArray(File[]::new);
bennyl

2
Điều làm cho Scala "tốt hơn" theo một số cách so với Java là các khả năng hệ thống kiểu mở rộng giúp dễ dàng thể hiện các mẫu chung hơn như các loại (như Monads, Functor, v.v.). Điều này cho phép bạn tạo các loại không cản trở bạn do các hợp đồng quá nghiêm ngặt, như thường xảy ra trong Java. Các hợp đồng nghiêm ngặt không dựa trên các mẫu thực tế trong mã là lý do Mẫu đảo ngược trách nhiệm là cần thiết chỉ để kiểm tra đơn vị mã của bạn một cách chính xác (Dependence Injection xuất hiện trước tiên và Địa ngục XML mà nó mang lại). Các addl. sự đồng nhất mà sự linh hoạt mang lại chỉ là một phần thưởng.
josiah

67

Viết Scala của bạn như Java và bạn có thể mong đợi mã byte gần như giống hệt nhau được phát ra - với các số liệu gần như giống hệt nhau.

Viết nó "thành ngữ" hơn, với các đối tượng bất biến và các hàm bậc cao hơn, và nó sẽ chậm hơn một chút và lớn hơn một chút. Một ngoại lệ cho quy tắc này là khi sử dụng các đối tượng chung trong đó các tham số loại sử dụng@specialised chú thích, điều này sẽ tạo ra mã byte lớn hơn thậm chí có thể vượt quá hiệu suất của Java bằng cách tránh đấm bốc / bỏ hộp.

Một điều đáng nói nữa là thực tế là bộ nhớ nhiều hơn / tốc độ thấp hơn là sự đánh đổi không thể tránh khỏi khi viết mã có thể chạy song song. Mã Scala thành ngữ về bản chất có tính khai báo cao hơn nhiều so với mã Java thông thường và thường chỉ có 4 ký tự (.par ) không hoàn toàn song song.

Vì vậy nếu

  • Mã Scala mất 1,25 lần so với mã Java trong một luồng
  • Nó có thể dễ dàng phân chia trên 4 lõi (hiện phổ biến ngay cả trong máy tính xách tay)
  • trong thời gian chạy song song là (1,24 / 4 =) 0,3125x Java gốc

Sau đó, bạn sẽ nói rằng mã Scala bây giờ chậm hơn 25% hoặc nhanh hơn 3 lần?

Câu trả lời đúng phụ thuộc vào chính xác cách bạn xác định "hiệu suất" :)


4
Ngẫu nhiên, bạn có thể muốn đề cập đến .partrong 2.9.
Rex Kerr

26
>> Sau đó, bạn có nói rằng mã Scala bây giờ chậm hơn 25% hoặc nhanh hơn 3 lần không? << Tôi muốn nói tại sao không phải là giả thuyết so sánh của bạn với mã Java đa luồng?
igouy

17
@igouy - Vấn đề là mã giả định không tồn tại, bản chất bắt buộc của mã Java "nhanh hơn" khiến cho việc song song hóa trở nên khó khăn hơn nhiều, do đó tỷ lệ chi phí / lợi ích có nghĩa là không có khả năng xảy ra. Scala thành ngữ, mặt khác, có tính khai báo cao hơn nhiều có thể thường xuyên được thực hiện đồng thời với không nhiều hơn một thay đổi nhỏ.
Kevin Wright

7
Sự tồn tại của các chương trình Java đồng thời không có nghĩa là một chương trình Java điển hình có thể dễ dàng thích nghi với sự tương tranh. Nếu có bất cứ điều gì, tôi muốn nói rằng kiểu kết hợp ngã ba cụ thể đặc biệt hiếm gặp trong Java và phải được mã hóa rõ ràng, trong khi các thao tác đơn giản như tìm giá trị chứa tối thiểu hoặc tổng các giá trị trong bộ sưu tập có thể được thực hiện song song trong Scala bằng cách sử dụng đơn giản .par.
Kevin Wright

5
Không, tôi có thể không. Loại điều này là một khối xây dựng cơ bản cho nhiều thuật toán và để thấy nó hiện diện ở mức độ thấp như vậy trong các thư viện tiêu chuẩn và ngôn ngữ (cùng các thư viện tiêu chuẩn mà tất cả các chương trình sẽ sử dụng, không chỉ các điển hình) là bằng chứng cho thấy bạn ' đã gần hơn để được đồng thời bằng cách chỉ cần chọn ngôn ngữ. Ví dụ, ánh xạ trên một bộ sưu tập vốn đã phù hợp để song song hóa và số lượng chương trình Scala không sử dụng mapphương pháp này sẽ rất nhỏ.
Kevin Wright

31

Trò chơi điểm chuẩn ngôn ngữ máy tính:

Kiểm tra tốc độ java / scala 1.71 / 2.25

Kiểm tra bộ nhớ java / scala 66,55 / 80,81

Vì vậy, điểm chuẩn này nói rằng java nhanh hơn 24% và scala sử dụng bộ nhớ nhiều hơn 21%.

Tất cả trong tất cả, đó không phải là vấn đề lớn và không quan trọng trong các ứng dụng trong thế giới thực, nơi phần lớn thời gian được sử dụng bởi cơ sở dữ liệu và mạng.

Điểm mấu chốt: Nếu Scala làm cho bạn và nhóm của bạn (và mọi người thực hiện dự án khi bạn rời đi) hiệu quả hơn, thì bạn nên thực hiện nó.


34
Kích thước mã java / scala 3,39 / 2,21
hammar

22
Hãy cẩn thận với những con số như thế này, chúng có vẻ rất chính xác trong khi thực tế chúng gần như không có gì. Không phải là trung bình Scala luôn nhanh hơn 24% so với Java, v.v.
Jesper

3
Các số trích dẫn của Afaik chỉ ra điều ngược lại: Java nhanh hơn 24% so với scala. Nhưng như bạn nói - chúng là microbenchmark, không cần phải khớp với những gì đang xảy ra trong các ứng dụng thực. Và cách khác nhau hoặc giải pháp vấn đề trong các ngôn ngữ khác nhau có thể dẫn đến các chương trình ít so sánh cuối cùng.
người dùng không biết

9
"Nếu Scala làm cho bạn và nhóm của bạn ..." Dòng dưới cùng: Bạn sẽ biết rằng sau đó không trước :-)
igouy

Trang Trợ giúp trò chơi điểm chuẩn cung cấp một ví dụ về cách "So sánh tốc độ và kích thước chương trình cho 2 lần triển khai ngôn ngữ". Đối với Scala và Java, trang web so sánh thích hợp là - Shootout.alioth.debian.org/u64q/scala.php
igouy

20

Những người khác đã trả lời câu hỏi này liên quan đến các vòng lặp chặt chẽ mặc dù dường như có sự khác biệt rõ ràng về hiệu suất giữa các ví dụ của Rex Kerr mà tôi đã nhận xét.

Câu trả lời này thực sự nhắm vào những người có thể điều tra nhu cầu tối ưu hóa chặt chẽ như lỗ hổng thiết kế.

Tôi còn khá mới với Scala (khoảng một năm hoặc lâu hơn) nhưng cảm giác về nó, cho đến nay, là nó cho phép bạn trì hoãn nhiều khía cạnh của thiết kế, thực hiện và thực hiện tương đối dễ dàng (với đủ khả năng đọc và thử nghiệm nền :)

Các tính năng thiết kế hoãn lại:

Các tính năng thực hiện bị trì hoãn:

Các tính năng thực thi bị trì hoãn: (xin lỗi, không có liên kết)

  • Giá trị lười biếng an toàn chủ đề
  • Tên qua
  • Thứ đơn nguyên

Những tính năng này, với tôi, là những tính năng giúp chúng ta bước đi trên con đường đến với các ứng dụng nhanh, chặt chẽ.


Các ví dụ của Rex Kerr khác nhau về khía cạnh thực thi được hoãn lại. Trong ví dụ Java, việc phân bổ bộ nhớ được hoãn lại cho đến khi kích thước của nó được tính toán trong đó ví dụ Scala trì hoãn việc tra cứu ánh xạ. Đối với tôi, chúng có vẻ như là các thuật toán hoàn toàn khác nhau.

Đây là những gì tôi nghĩ là nhiều hơn một quả táo tương đương với ví dụ Java của mình:

val bigEnough = array.collect({
    case k: String if k.length > 2 && mapping.contains(k) => mapping(k)
})

Không có bộ sưu tập trung gian, không có Optiontrường hợp, v.v ... Điều này cũng bảo tồn loại bộ sưu tập nên bigEnoughloại là Array[File]- Array'scollect thực hiện có thể sẽ được làm một cái gì đó dọc theo dòng của những gì mã Java của ông Kerr làm.

Các tính năng thiết kế bị trì hoãn mà tôi liệt kê ở trên cũng sẽ cho phép các nhà phát triển API bộ sưu tập của Scala triển khai triển khai thu thập cụ thể theo mảng nhanh đó trong các bản phát hành trong tương lai mà không phá vỡ API. Đây là những gì tôi đang đề cập với bước đi trên con đường đến tốc độ.

Cũng thế:

val bigEnough = array.withFilter(_.length > 2).flatMap(mapping.get)

Các withFilterphương pháp mà tôi đã sử dụng ở đây thay vì filtersửa chữa các vấn đề thu thập trung gian nhưng vẫn còn là vấn đề lựa chọn ví dụ.


Một ví dụ về tốc độ thực hiện đơn giản trong Scala là ghi nhật ký.

Trong Java, chúng ta có thể viết một cái gì đó như:

if (logger.isDebugEnabled())
    logger.debug("trace");

Trong Scala, đây chỉ là:

logger.debug("trace")

bởi vì tham số thông báo để gỡ lỗi trong Scala có kiểu " => String" mà tôi nghĩ là hàm không tham số thực thi khi được đánh giá, nhưng tài liệu gọi tên là pass-by-name.

EDIT {Hàm trong Scala là các đối tượng nên có thêm một đối tượng ở đây. Đối với công việc của tôi, trọng lượng của một đối tượng tầm thường đáng để loại bỏ khả năng thông điệp tường trình được đánh giá không cần thiết. }

Điều này không làm cho mã nhanh hơn nhưng nó làm cho nó có khả năng nhanh hơn và chúng ta ít có kinh nghiệm trải qua và dọn dẹp mã của người khác.

Đối với tôi, đây là một chủ đề nhất quán trong Scala.


Mã cứng không nắm bắt được tại sao Scala nhanh hơn mặc dù nó gợi ý một chút.

Tôi cảm thấy đó là sự kết hợp giữa việc sử dụng lại mã và mức chất lượng mã trong Scala.

Trong Java, mã tuyệt vời thường bị buộc phải trở thành một mớ hỗn độn không thể hiểu được và do đó không thực sự khả thi trong các API chất lượng sản xuất vì hầu hết các lập trình viên sẽ không thể sử dụng nó.

Tôi rất hy vọng rằng Scala có thể cho phép các einstein trong chúng ta triển khai các API có thẩm quyền hơn nhiều, có khả năng được thể hiện thông qua DSL. Các API cốt lõi trong Scala đã đi xa trên con đường này.


Công cụ ghi nhật ký của bạn là một ví dụ tốt cho các cạm bẫy hiệu suất của Scala: logger.debug ("dấu vết") tạo ra một đối tượng mới cho hàm không tham số.
jcsahnwaldt phục hồi Monica

Thật vậy - điều đó ảnh hưởng đến điểm liên quan của tôi như thế nào?
Seth

Các đối tượng nói trên cũng có thể được sử dụng để tạo các cấu trúc điều khiển IoC trong suốt vì mục đích hiệu quả. Đúng, kết quả tương tự về mặt lý thuyết là có thể có trong Java nhưng nó sẽ là thứ gây ảnh hưởng / làm xáo trộn đáng kể cách viết mã - do đó, lập luận của tôi rằng Scala của việc trì hoãn nhiều yếu tố phát triển phần mềm giúp chúng ta tiến tới mã nhanh hơn - nhiều khả năng là nhanh hơn trong thực tế so với hiệu suất đơn vị nhanh hơn một chút.
Seth

Ok, tôi đã đọc lại và tôi đã viết, "tốc độ thực hiện đơn giản" - tôi sẽ thêm một ghi chú. Điểm tốt :)
Seth

3
Có thể dự đoán được nếu câu lệnh (về cơ bản là miễn phí trên bộ xử lý siêu thanh) so với phân bổ đối tượng + rác. Mã Java rõ ràng là nhanh hơn (lưu ý rằng nó chỉ đánh giá điều kiện, việc thực thi sẽ không đạt được câu lệnh log.) Để đáp ứng với "Đối với công việc của tôi, trọng lượng của một đối tượng tầm thường đáng để loại bỏ khả năng thông điệp tường trình được đánh giá một cách không cần thiết . "
Eloff


10

Cả Java và Scala đều biên dịch thành mã byte JVM, vì vậy sự khác biệt không lớn lắm. Sự so sánh tốt nhất bạn có thể nhận được có lẽ là trên trò chơi điểm chuẩn ngôn ngữ máy tính , về cơ bản nói rằng cả Java và Scala đều có cùng mức sử dụng bộ nhớ. Scala chỉ một chút chậm hơn so với Java trên một số điểm chuẩn được liệt kê, nhưng điều đó có thể đơn giản là do việc triển khai các chương trình là khác nhau.

Thực sự, cả hai đều rất thân thiết, không đáng lo ngại. Việc tăng năng suất mà bạn có được bằng cách sử dụng một ngôn ngữ biểu cảm hơn như Scala đáng giá hơn rất nhiều so với hiệu suất tối thiểu (nếu có).


7
Tôi thấy một sai lầm logic ở đây: Cả hai ngôn ngữ đều biên dịch theo mã byte, nhưng một lập trình viên có kinh nghiệm và một người mới - mã của họ cũng biên dịch theo mã byte - nhưng không phải cùng mã byte, do đó, kết luận, sự khác biệt không thể lớn đến thế , có thể sai. Và trên thực tế, trong thời gian trước đây, một vòng lặp while có thể nhanh hơn nhiều, nhanh hơn nhiều so với vòng lặp tương đương về mặt ngữ nghĩa (nếu tôi nhớ chính xác, ngày nay nó tốt hơn nhiều). Và cả hai đã được biên dịch thành mã byte, tất nhiên.
người dùng không biết

@user unknown - "một vòng lặp while có thể nhanh hơn nhiều, nhanh hơn nhiều so với vòng lặp tương đương về mặt ngữ nghĩa" - lưu ý rằng các chương trình trò chơi điểm chuẩn Scala đó được viết bằng các vòng lặp.
igouy

@igouy: Tôi không nói về kết quả từ microbenchmark này, nhưng về tranh luận. Một tuyên bố thực sự Java and Scala both compile down to JVM bytecode, được kết hợp với một sotuyên bố trong câu hỏi diffence isn't that big.mà tôi muốn chỉ ra, đó sochỉ là một thủ thuật tu từ, và không phải là một kết luận tranh luận.
người dùng không biết

3
đáng ngạc nhiên trả lời không chính xác với số phiếu cao đáng ngạc nhiên.
shabunc

4

Ví dụ Java thực sự không phải là một thành ngữ cho các chương trình ứng dụng điển hình. Mã tối ưu hóa như vậy có thể được tìm thấy trong một phương pháp thư viện hệ thống. Nhưng sau đó, nó sẽ sử dụng một mảng đúng loại, tức là Tệp [] và sẽ không ném IndexOutOfBoundException. (Điều kiện lọc khác nhau để đếm và thêm). Phiên bản của tôi sẽ là (luôn luôn (!) Với các dấu ngoặc nhọn vì tôi không muốn mất một giờ để tìm kiếm một lỗi được giới thiệu bằng cách lưu 2 giây để nhấn một phím duy nhất trong Eclipse):

List<File> bigEnough = new ArrayList<File>();
for(String s : array) {
  if(s.length() > 2) {
    File file = mapping.get(s);
    if (file != null) {
      bigEnough.add(file);
    }
  }
}

Nhưng tôi có thể mang đến cho bạn rất nhiều ví dụ mã Java xấu xí khác từ dự án hiện tại của tôi. Tôi đã cố gắng tránh các bản sao chung và sửa đổi phong cách mã hóa bằng cách bao gồm các cấu trúc và hành vi chung.

Trong lớp cơ sở DAO trừu tượng của tôi, tôi có một lớp bên trong trừu tượng cho cơ chế lưu trữ chung. Đối với mỗi loại đối tượng mô hình cụ thể, có một lớp con của lớp cơ sở DAO trừu tượng, trong đó lớp bên trong được phân lớp để cung cấp một triển khai cho phương thức tạo đối tượng kinh doanh khi được tải từ cơ sở dữ liệu. (Chúng tôi không thể sử dụng công cụ ORM vì chúng tôi truy cập hệ thống khác thông qua API độc quyền.)

Mã phân lớp và mã khởi tạo này hoàn toàn không rõ ràng trong Java và sẽ rất dễ đọc trong Scala.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.