Là MATLAB OOP chậm hay tôi đang làm gì đó sai?


144

Tôi đang thử nghiệm với MATLAB OOP , như một sự khởi đầu Tôi bắt chước C của tôi ++ 's lớp Logger và tôi đặt tất cả các chức năng helper chuỗi của tôi trong một lớp String, suy nghĩ nó sẽ là tuyệt vời để có thể làm những việc như a + b, a == b, a.find( b )thay vì strcat( a b ), strcmp( a, b ), lấy phần tử đầu tiên của strfind( a, b ), v.v.

Vấn đề: chậm lại

Tôi đặt những thứ trên để sử dụng và ngay lập tức nhận thấy sự chậm lại mạnh mẽ . Tôi đã làm sai (điều này chắc chắn là có thể vì tôi có kinh nghiệm MATLAB khá hạn chế), hoặc OOP của MATLAB chỉ giới thiệu rất nhiều chi phí?

Trường hợp thử nghiệm của tôi

Đây là bài kiểm tra đơn giản mà tôi đã thực hiện đối với chuỗi, về cơ bản chỉ cần nối thêm một chuỗi và xóa phần được nối lại:

Lưu ý: Đừng thực sự viết một lớp String như thế này bằng mã thực! Matlab hiện có một stringkiểu mảng riêng và thay vào đó bạn nên sử dụng kiểu đó.

classdef String < handle
  ....
  properties
    stringobj = '';
  end
  function o = plus( o, b )
    o.stringobj = [ o.stringobj b ];
  end
  function n = Length( o )
    n = length( o.stringobj );
  end
  function o = SetLength( o, n )
    o.stringobj = o.stringobj( 1 : n );
  end
end

function atest( a, b ) %plain functions
  n = length( a );
  a = [ a b ];
  a = a( 1 : n );

function btest( a, b ) %OOP
  n = a.Length();
  a = a + b;
  a.SetLength( n );

function RunProfilerLoop( nLoop, fun, varargin )
  profile on;
  for i = 1 : nLoop
    fun( varargin{ : } );
  end
  profile off;
  profile report;

a = 'test';
aString = String( 'test' );
RunProfilerLoop( 1000, @(x,y)atest(x,y), a, 'appendme' );
RunProfilerLoop( 1000, @(x,y)btest(x,y), aString, 'appendme' );

Kết quả

Tổng thời gian tính bằng giây, trong 1000 lần lặp:

btest 0,550 (với String.SetLpm 0.138, String.plus 0.065, String.Lipse 0.057)

ít nhất 0,015

Các kết quả cho hệ thống logger cũng tương tự: 0,1 giây cho 1000 cuộc gọi đến frpintf( 1, 'test\n' ), 7 (!) Giây cho 1000 cuộc gọi đến hệ thống của tôi khi sử dụng lớp String bên trong (OK, nó có logic hơn rất nhiều, nhưng để so sánh với C ++: chi phí hoạt động của hệ thống của tôi sử dụng std::string( "blah" )std::coutở phía đầu ra so với đồng bằng std::cout << "blah"là theo thứ tự 1 mili giây.)

Có phải chỉ là chi phí khi tìm kiếm các chức năng lớp / gói?

Vì MATLAB được diễn giải, nó phải tra cứu định nghĩa của hàm / đối tượng trong thời gian chạy. Vì vậy, tôi đã tự hỏi rằng có thể nhiều chi phí hơn có liên quan đến việc tìm kiếm chức năng lớp hoặc gói so với các hàm có trong đường dẫn. Tôi đã thử kiểm tra điều này, và nó chỉ trở nên xa lạ. Để loại trừ ảnh hưởng của các lớp / đối tượng, tôi so sánh việc gọi một hàm trong đường dẫn và một hàm trong một gói:

function n = atest( x, y )
  n = ctest( x, y ); % ctest is in matlab path

function n = btest( x, y )
  n = util.ctest( x, y ); % ctest is in +util directory, parent directory is in path

Kết quả, được thu thập theo cách tương tự như trên:

ít nhất 0,004 giây, 0,001 giây trong ctest

btest 0,060 giây, 0,011 giây trong produc.ctest

Vì vậy, có phải tất cả chi phí này chỉ đến từ MATLAB dành thời gian tìm kiếm các định nghĩa cho việc triển khai OOP của nó, trong khi chi phí này không có cho các chức năng trực tiếp trong đường dẫn?


5
Cảm ơn bạn cho câu hỏi này! Hiệu suất của đống Matlab (OOP / bao đóng) đã gây rắc rối cho tôi trong nhiều năm, xem stackoverflow.com/questions/1446281/matlabs-garbage-collector . Tôi thực sự tò mò về những gì MatlabDoug / Loren / MikeKatz sẽ trả lời cho bài viết của bạn.
Mikhail

1
^ đó là một đọc thú vị.
stijn

1
@MatlabDoug: có lẽ đồng nghiệp của bạn Mike Karr có thể nhận xét OP?
Mikhail

4
Độc giả cũng nên kiểm tra bài đăng trên blog gần đây này (của Dave Foti) thảo luận về hiệu suất OOP trong phiên bản R2012a mới nhất: Xem xét hiệu suất trong Mã MATLAB hướng đối tượng
Amro

1
Một ví dụ đơn giản về độ nhạy trên cấu trúc mã trong đó lệnh gọi các phương thức của các phần tử con được đưa ra khỏi vòng lặp. for i = 1:this.get_n_quantities() if(strcmp(id,this.get_quantity_rlz(i).get_id())) ix = i; end endmất 2,2 giây, trong khi nq = this.get_n_quantities(); a = this.get_quantity_realizations(); for i = 1:nq c = a{i}; if(strcmp(id,c.get_id())) ix = i; end endmất 0,01, hai đơn đặt hàng của mag
Jose Ospina

Câu trả lời:


223

Tôi đã làm việc với OO MATLAB được một thời gian và cuối cùng đã xem xét các vấn đề hiệu suất tương tự.

Câu trả lời ngắn gọn là: có, OOP của MATLAB là loại chậm. Có rất nhiều phương thức gọi phương thức, cao hơn các ngôn ngữ OO chính thống và bạn không thể làm gì nhiều với nó. Một phần lý do có thể là MATLAB thành ngữ sử dụng mã "được vector hóa" để giảm số lượng cuộc gọi phương thức và chi phí cho mỗi cuộc gọi không phải là ưu tiên cao.

Tôi đã đánh giá hiệu năng bằng cách viết các hàm "nop" không làm gì như các loại hàm và phương thức khác nhau. Dưới đây là một số kết quả điển hình.

>> tên gọi
Máy tính: PCWIN Phát hành: 2009b
Gọi mỗi chức năng / phương thức 100000 lần
Hàm nop (): 0,02261 giây 0,23 usec mỗi cuộc gọi
Hàm nop1-5 (): 0,02182 giây 0,22 usec mỗi cuộc gọi
chức năng con nop (): 0,02244 giây 0,22 usec mỗi cuộc gọi
@ () [] chức năng ẩn danh: 0,08461 giây 0,85 usec mỗi cuộc gọi
phương pháp nop (obj): 0,24664 giây 2,47 usec mỗi cuộc gọi
Các phương thức nop1-5 (obj): 0,23469 giây 2,35 usec mỗi cuộc gọi
Hàm riêng tư nop (): 0,02197 giây 0,22 usec mỗi cuộc gọi
classdef nop (obj): 0.90547 giây 9,05 usec mỗi cuộc gọi
classdef obj.nop (): 1.75522 giây 17.55 usec mỗi cuộc gọi
classdef private_nop (obj): 0.84738 giây 8.47 usec mỗi cuộc gọi
classdef nop (obj) (m-file): 0.90560 giây 9.06 usec mỗi cuộc gọi
classdef class.staticnop (): 1.16361 giây 11,64 usec mỗi cuộc gọi
Java nop (): 2.43035 giây 24.30 usec mỗi cuộc gọi
Java static_nop (): 0.87682 giây 8.77 usec mỗi cuộc gọi
Java nop () từ Java: 0,00014 giây 0,00 usec mỗi cuộc gọi
MEX mexnop (): 0.11409 giây 1.14 usec mỗi cuộc gọi
C nop (): 0,00001 giây 0,00 usec mỗi cuộc gọi

Kết quả tương tự trên R2008a đến R2009b. Đây là trên Windows XP x64 chạy MATLAB 32 bit.

"Java nop ()" là một phương thức Java không có gì được gọi từ bên trong một vòng mã M và bao gồm cả chi phí gửi MATLAB-to-Java với mỗi cuộc gọi. "Java nop () từ Java" là điều tương tự được gọi trong vòng lặp Java for () và không phải chịu hình phạt ranh giới đó. Lấy thời gian Java và C với một hạt muối; một trình biên dịch thông minh có thể tối ưu hóa hoàn toàn các cuộc gọi đi.

Cơ chế phạm vi gói là mới, được giới thiệu cùng lúc với các lớp classdef. Hành vi của nó có thể liên quan.

Một vài kết luận dự kiến:

  • Các phương thức chậm hơn các hàm.
  • Các phương thức kiểu mới (classdef) chậm hơn các phương thức kiểu cũ.
  • obj.nop()Cú pháp mới chậm hơn nop(obj)cú pháp, ngay cả đối với cùng một phương thức trên một đối tượng classdef. Tương tự cho các đối tượng Java (không hiển thị). Nếu bạn muốn đi nhanh, hãy gọi nop(obj).
  • Chi phí cuộc gọi phương thức cao hơn (khoảng 2 lần) trong MATLAB 64 bit trên Windows. (Không được hiển thị.)
  • Công văn phương thức MATLAB chậm hơn một số ngôn ngữ khác.

Nói tại sao điều này là như vậy sẽ chỉ là suy đoán về phía tôi. Nội bộ OO của công cụ MATLAB không công khai. Đây không phải là vấn đề được giải thích và được biên dịch theo từng se - MATLAB có JIT - nhưng cách gõ và cú pháp lỏng lẻo hơn của MATLAB có thể có nghĩa là làm việc nhiều hơn trong thời gian chạy. (Ví dụ: bạn không thể biết từ cú pháp một mình liệu "f (x)" là lệnh gọi hàm hay chỉ mục thành một mảng; nó phụ thuộc vào trạng thái của không gian làm việc trong thời gian chạy.) Có thể là do các định nghĩa lớp của MATLAB bị ràng buộc đến trạng thái hệ thống tập tin theo cách mà nhiều ngôn ngữ khác không có.

Vậy lam gi?

Một cách tiếp cận MATLAB thành ngữ cho vấn đề này là "vector hóa" mã của bạn bằng cách cấu trúc các định nghĩa lớp của bạn sao cho một cá thể đối tượng bao bọc một mảng; nghĩa là, mỗi trường của nó chứa các mảng song song (được gọi là tổ chức "phẳng" trong tài liệu MATLAB). Thay vì có một mảng các đối tượng, mỗi trường có các giá trị vô hướng, xác định các đối tượng là mảng và có các phương thức lấy mảng làm đầu vào và thực hiện các cuộc gọi được vector hóa trên các trường và đầu vào. Điều này làm giảm số lượng các cuộc gọi phương thức được thực hiện, hy vọng đủ rằng chi phí công văn không phải là một nút cổ chai.

Bắt chước một lớp C ++ hoặc Java trong MATLAB có lẽ sẽ không tối ưu. Các lớp Java / C ++ thường được xây dựng sao cho các đối tượng là các khối xây dựng nhỏ nhất, cụ thể nhất có thể (nghĩa là có rất nhiều lớp khác nhau) và bạn kết hợp chúng thành các mảng, các đối tượng tập hợp, v.v. và lặp lại chúng bằng các vòng lặp. Để tạo các lớp MATLAB nhanh, hãy chuyển phương pháp đó từ trong ra ngoài. Có các lớp lớn hơn có các trường là các mảng và gọi các phương thức vector hóa trên các mảng đó.

Vấn đề là sắp xếp mã của bạn để chơi theo các điểm mạnh của ngôn ngữ - xử lý mảng, toán học véc tơ - và tránh các điểm yếu.

EDIT: Kể từ bài đăng gốc, R2010b và R2011a đã xuất hiện. Bức tranh tổng thể là như nhau, với các cuộc gọi MCOS trở nên nhanh hơn một chút và các cuộc gọi phương thức kiểu cũ và Java ngày càng chậm hơn .

EDIT: Tôi đã từng có một số lưu ý ở đây về "độ nhạy đường dẫn" với bảng thời gian gọi chức năng bổ sung, trong đó thời gian chức năng bị ảnh hưởng bởi cách đường dẫn Matlab được định cấu hình, nhưng dường như đó là sự quang sai trong thiết lập mạng cụ thể của tôi tại thời gian. Biểu đồ trên phản ánh thời gian điển hình của tính ưu việt của các bài kiểm tra của tôi theo thời gian.

Cập nhật: R2011b

EDIT (2/13/2012): R2011b đã hết, và hình ảnh hiệu suất đã thay đổi đủ để cập nhật điều này.

Arch: PCWIN Phát hành: 2011b 
Máy: R2011b, Windows XP, 8x Core i7-2600 @ 3.40GHz, RAM 3 GB, NVIDIA NVS 300
Thực hiện mỗi thao tác 100000 lần
tổng số phong cách trên mỗi cuộc gọi
Hàm nop (): 0,01578 0,16
nop (), hủy vòng lặp 10 lần: 0,01477 0,15
nop (), hủy vòng lặp 100x: 0,01518 0,15
chức năng con nop (): 0,01559 0,16
@ () [] Hàm ẩn danh: 0,06400 0,64
phương pháp nop (obj): 0,28482 2,85
nop () hàm riêng: 0,01505 0,15
classdef nop (obj): 0.43323 4.33
classdef obj.nop (): 0.81087 8.11
classdef private_nop (obj): 0.32272 3.23
classdef class.staticnop (): 0.88959 8.90
hằng số classdef: 1.51890 15.19
tài sản classdef: 0,12992 1,30
thuộc tính classdef với getter: 1.39912 13.99
+ Hàm pkg.nop (): 0.87345 8.73
+ pkg.nop () từ bên trong + pkg: 0.80501 8.05
Java obj.nop (): 1.86378 18.64
Java nop (obj): 0.22645 2.26
Java feval ('nop', obj): 0,52544 5,25
Java Klass.static_nop (): 0,35357 3,54
Java obj.nop () từ Java: 0,00010 0,00
MEXnop (): 0,08709 0,87
C nop (): 0,00001 0,00
j () (dựng sẵn): 0,00251 0,03

Tôi nghĩ rằng kết quả cuối cùng là:

  • Phương pháp MCOS / classdef nhanh hơn. Chi phí bây giờ ngang bằng với các lớp kiểu cũ, miễn là bạn sử dụng foo(obj)cú pháp. Vì vậy, tốc độ phương thức không còn là lý do để gắn bó với các lớp kiểu cũ trong hầu hết các trường hợp. (Kudos, MathWorks!)
  • Đặt các chức năng trong không gian tên làm cho chúng chậm. (Không mới trong R2011b, chỉ mới trong thử nghiệm của tôi.)

Cập nhật: R2014a

Tôi đã xây dựng lại mã điểm chuẩn và chạy nó trên R2014a.

Matlab R2014a trên PCWIN64  
Matlab 8.3.0.532 (R2014a) / Java 1.7.0_11 trên PCWIN64 Windows 7 6.1 (eilonwy-win7) 
Máy: CPU Core i7-3615QM @ 2.30GHz, RAM 4 GB (Nền tảng ảo VMware)
nIters = 100000 

Thời gian hoạt động (Giansec)  
hàm nop (): 0,14 
hàm con nop (): 0,14 
@ () [] Hàm ẩn danh: 0,69 
phương pháp nop (obj): 3.28 
nop () fcn riêng trên @ class: 0.14 
classdef nop (obj): 5.30 
classdef obj.nop (): 10,78 
classdef pivate_nop (obj): 4,88 
classdef class.static_nop (): 11.81 
hằng số classdef: 4.18 
tài sản hạng nhất: 1,18 
tài sản classdef với getter: 19,26 
+ Hàm pkg.nop (): 4.03 
+ pkg.nop () từ bên trong + pkg: 4.16 
feval ('nop'): 2,31 
feval (@nop): 0,22 
eval ('nop'): 59,46 
Java obj.nop (): 26,07 
Java nop (obj): 3,72 
Java feval ('nop', obj): 9,25 
Java Klass.staticNop (): 10,54 
Java obj.nop () từ Java: 0,01 
MEXnop (): 0,91 
dựng sẵn j (): 0,02 
cấu trúc truy cập trường s.foo: 0.14 
isempty (liên tục): 0,00 

Cập nhật: R2015b: Đối tượng đã nhanh hơn!

Đây là kết quả R2015b, được cung cấp bởi @Shaken. Đây là một thay đổi lớn : OOP nhanh hơn đáng kể và bây giờ obj.method()cú pháp nhanh như method(obj)và nhanh hơn nhiều so với các đối tượng OOP cũ.

Matlab R2015b trên PCWIN64  
Matlab 8.6.0.267246 (R2015b) / Java 1.7.0_60 trên PCWIN64 Windows 8 6.2 (lắc nanit) 
Máy: CPU Core i7-4720HQ @ 2.60GHz, RAM 16 GB (20378)
nIters = 100000 

Thời gian hoạt động (Giansec)  
Hàm nop (): 0,04 
hàm con nop (): 0,08 
@ () [] hàm ẩn danh: 1,83 
phương pháp nop (obj): 3.15 
nop () fcn riêng trên @ class: 0,04 
classdef nop (obj): 0,28 
classdef obj.nop (): 0,31 
classdef pivate_nop (obj): 0,34 
classdef class.static_nop (): 0,05 
hằng số classdef: 0,25 
tài sản classdef: 0,25 
thuộc tính classdef với getter: 0,64 
+ hàm pkg.nop (): 0,04 
+ pkg.nop () từ bên trong + pkg: 0,04 
feval ('nop'): 8.26 
feval (@nop): 0,63 
eval ('nop'): 21,22 
Java obj.nop (): 14,15 
Java nop (obj): 2.50 
Java feval ('nop', obj): 10.30 
Java Klass.staticNop (): 24,48 
Java obj.nop () từ Java: 0,01 
MEXnop (): 0,33 
dựng sẵn j (): 0,15 
cấu trúc truy cập trường s.foo: 0,25 
isempty (liên tục): 0,13 

Cập nhật: R2018a

Đây là kết quả R2018a. Đó không phải là bước nhảy lớn mà chúng ta đã thấy khi công cụ thực thi mới được giới thiệu vào R2015b, nhưng nó vẫn là một năm đáng kể so với cải tiến hàng năm. Đáng chú ý, xử lý chức năng ẩn danh có cách nhanh hơn.

Matlab R2018a trên MACI64  
Matlab 9.4.0.813654 (R2018a) / Java 1.8.0_144 trên MACI64 Mac OS X 10.13.5 (eilonwy) 
Máy: CPU Core i7-3615QM @ 2.30GHz, RAM 16 GB 
nIters = 100000 

Thời gian hoạt động (Giansec)  
hàm nop (): 0,03 
hàm con nop (): 0,04 
@ () [] hàm ẩn danh: 0,16 
classdef nop (obj): 0,16 
classdef obj.nop (): 0,17 
classdef pivate_nop (obj): 0.16 
classdef class.static_nop (): 0,03 
hằng số classdef: 0,16 
tài sản classdef: 0,13 
thuộc tính classdef với getter: 0,39 
+ Hàm pkg.nop (): 0,02 
+ pkg.nop () từ bên trong + pkg: 0,02 
feval ('nop'): 15.62 
feval (@nop): 0,43 
eval ('nop'): 32,08 
Java obj.nop (): 28,77 
Java nop (obj): 8.02 
Java feval ('nop', obj): 21,85 
Java Klass.staticNop (): 45,49 
Java obj.nop () từ Java: 0,03 
MEXnop (): 3,54 
dựng sẵn j (): 0.10 
cấu trúc truy cập trường s.foo: 0.16 
isempty (liên tục): 0,07 

Cập nhật: R2018b và R2019a: Không thay đổi

Không có thay đổi đáng kể. Tôi không bận tâm để bao gồm các kết quả kiểm tra.

Mã nguồn cho điểm chuẩn

Tôi đã đặt mã nguồn cho các điểm chuẩn này trên GitHub, được phát hành theo Giấy phép MIT. https://github.com/apjanke/matlab-bench


5
@AndrewJanke Bạn có nghĩ rằng bạn có thể chạy lại điểm chuẩn với R2012a không? Điều này thực sự thú vị.
Đặng Khoa

7
Chào các bạn. Nếu bạn vẫn quan tâm đến mã nguồn, tôi đã xây dựng lại nó và mở nguồn trên GitHub. github.com/apjanke/matlab-bench
Andrew Janke

2
@Seeda: Các phương thức tĩnh được liệt kê là "classdef class.static_nop ()" trong các kết quả này. Chúng khá chậm so với các chức năng. Nếu họ không được gọi thường xuyên, điều đó không thành vấn đề.
Andrew Janke


2
Ồ Nếu những kết quả đó giữ vững, tôi có thể cần phải xem lại toàn bộ câu trả lời này. Thêm. Cảm ơn!
Andrew Janke

3

Lớp xử lý có một chi phí bổ sung từ việc theo dõi tất cả các tham chiếu đến chính nó cho mục đích dọn dẹp.

Hãy thử cùng một thí nghiệm mà không sử dụng lớp xử lý và xem kết quả của bạn là gì.


1
chính xác cùng một thử nghiệm với String, nhưng bây giờ là một lớp giá trị (mặc dù trên một máy khác); ít nhất: 0,009, btest: o.356. Về cơ bản, đó là sự khác biệt tương tự như với tay cầm, vì vậy tôi không nghĩ các tài liệu tham khảo theo dõi là câu trả lời chính. Nó cũng không giải thích chi phí hoạt động so với chức năng trong các gói.
stijn

Phiên bản matlab nào bạn đang sử dụng?
MikeEL

1
Tôi đã chạy một số so sánh tương tự giữa các lớp xử lý và giá trị và không nhận thấy sự khác biệt về hiệu năng giữa hai lớp.
RjOllos

Tôi cũng không còn nhận thấy một sự khác biệt.
MikeEL

Có nghĩa là: trong Matlab, tất cả các mảng, không chỉ xử lý các đối tượng, đều được tính tham chiếu, bởi vì chúng sử dụng bản sao trên ghi và chia sẻ dữ liệu thô bên dưới.
Andrew Janke

1

Hiệu suất OO phụ thuộc đáng kể vào phiên bản MATLAB được sử dụng. Tôi không thể nhận xét về tất cả các phiên bản, nhưng biết từ kinh nghiệm rằng 2012a được cải thiện hơn nhiều so với các phiên bản 2010. Không có điểm chuẩn và vì vậy không có số để trình bày. Mã của tôi, được viết riêng bằng cách sử dụng các lớp xử lý và được viết trong năm 2012a sẽ không chạy ở tất cả các phiên bản trước.


1

Trên thực tế không có vấn đề với mã của bạn nhưng nó là một vấn đề với Matlab. Tôi nghĩ rằng trong đó là một loại chơi xung quanh để trông như thế nào. Không có gì hơn chi phí để biên dịch mã lớp. Tôi đã thực hiện bài kiểm tra với điểm lớp đơn giản (một lần là xử lý) và điểm khác (một lần là lớp giá trị)

    classdef Pointh < handle
    properties
       X
       Y
    end  
    methods        
        function p = Pointh (x,y)
            p.X = x;
            p.Y = y;
        end        
        function  d = dist(p,p1)
            d = (p.X - p1.X)^2 + (p.Y - p1.Y)^2 ;
        end

    end
end

đây là bài kiểm tra

%handle points 
ph = Pointh(1,2);
ph1 = Pointh(2,3);

%values  points 
p = Pointh(1,2);
p1 = Pointh(2,3);

% vector points
pa1 = [1 2 ];
pa2 = [2 3 ];

%Structur points 
Ps.X = 1;
Ps.Y = 2;
ps1.X = 2;
ps1.Y = 3;

N = 1000000;

tic
for i =1:N
    ph.dist(ph1);
end
t1 = toc

tic
for i =1:N
    p.dist(p1);
end
t2 = toc

tic
for i =1:N
    norm(pa1-pa2)^2;
end
t3 = toc

tic
for i =1:N
    (Ps.X-ps1.X)^2+(Ps.Y-ps1.Y)^2;
end
t4 = toc

Kết quả t1 =

12.0212% Xử lý

t2 =

12,0042% giá trị

t3 =

0.5489  % vector

t4 =

0.0707 % structure 

Do đó, để thực hiện hiệu quả, tránh sử dụng OOP thay vào đó, cấu trúc là lựa chọn tốt để nhóm các biến

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.