Có phải là một thực hành tốt để sử dụng các loại dữ liệu nhỏ hơn cho các biến để tiết kiệm bộ nhớ?


32

Khi tôi học ngôn ngữ C ++ lần đầu tiên tôi đã học được rằng ngoài int, float v.v., các phiên bản nhỏ hơn hoặc lớn hơn của các loại dữ liệu này tồn tại trong ngôn ngữ. Ví dụ tôi có thể gọi một biến x

int x;
or 
short int x;

Sự khác biệt chính là int ngắn chiếm 2 byte bộ nhớ trong khi int chiếm 4 byte và int ngắn có giá trị nhỏ hơn, nhưng chúng ta cũng có thể gọi đây là để làm cho nó nhỏ hơn nữa:

int x;
short int x;
unsigned short int x;

mà thậm chí còn hạn chế hơn.

Câu hỏi của tôi ở đây là nếu đó là một cách thực hành tốt để sử dụng các loại dữ liệu riêng biệt theo giá trị mà biến của bạn thực hiện trong chương trình. Có phải là một ý tưởng tốt để luôn luôn khai báo các biến theo các loại dữ liệu này?


3
Bạn có biết mẫu thiết kế Flykg không? "một đối tượng giảm thiểu việc sử dụng bộ nhớ bằng cách chia sẻ càng nhiều dữ liệu càng tốt với các đối tượng tương tự khác; đó là cách sử dụng các đối tượng với số lượng lớn khi một biểu diễn lặp lại đơn giản sẽ sử dụng một lượng bộ nhớ không thể chấp nhận ..."
gnat

5
Với các cài đặt trình biên dịch đóng gói / căn chỉnh tiêu chuẩn, các biến sẽ được căn chỉnh theo ranh giới 4 byte, do đó có thể không có bất kỳ sự khác biệt nào cả.
nikie

36
Trường hợp cổ điển của tối ưu hóa sớm.
Scarfridge

1
@nikie - chúng có thể được căn chỉnh trên ranh giới 4 byte trên bộ xử lý x86 nhưng nói chung điều này không đúng. MSP430 đặt char trên bất kỳ địa chỉ byte nào và mọi thứ khác trên địa chỉ byte chẵn. Tôi nghĩ rằng AVR-32 và ARM Cortex-M là như nhau.
uɐɪ

3
Phần thứ 2 của câu hỏi của bạn ngụ ý rằng việc thêm unsignedbằng cách nào đó làm cho một số nguyên chiếm ít không gian hơn, tất nhiên là sai. Nó sẽ có cùng số lượng các giá trị đại diện riêng biệt (cho hoặc nhận 1 tùy thuộc vào cách biểu thị dấu hiệu) nhưng chỉ chuyển riêng sang dương.
underscore_d

Câu trả lời:


41

Hầu hết thời gian chi phí không gian là không đáng kể và bạn không nên lo lắng về nó, tuy nhiên bạn nên lo lắng về thông tin bổ sung mà bạn đang cung cấp bằng cách khai báo một loại. Ví dụ: nếu bạn:

unsigned int salary;

Bạn đang đưa ra một thông tin hữu ích cho một nhà phát triển khác: tiền lương không thể âm.

Sự khác biệt giữa ngắn, int, long hiếm khi gây ra vấn đề về không gian trong ứng dụng của bạn. Bạn có nhiều khả năng vô tình đưa ra giả định sai lầm rằng một số sẽ luôn phù hợp với một số kiểu dữ liệu. Có thể an toàn hơn khi luôn sử dụng int trừ khi bạn chắc chắn 100% số của bạn sẽ luôn rất nhỏ. Thậm chí sau đó, nó không có khả năng giúp bạn tiết kiệm bất kỳ dung lượng đáng chú ý nào.


5
Đúng là nó hiếm khi gây ra vấn đề trong những ngày này, nhưng nếu bạn đang thiết kế một thư viện hoặc một lớp mà một nhà phát triển khác sẽ sử dụng, thì đó lại là một vấn đề khác. Có thể họ sẽ cần lưu trữ cho một triệu đối tượng này, trong trường hợp chênh lệch là lớn - 4MB so với 2 MB chỉ cho một trường này.
dodgy_coder

30
Sử dụng unsignedtrong trường hợp này là một ý tưởng tồi: không chỉ tiền lương không thể âm, mà sự khác biệt giữa hai mức lương cũng không thể âm. (Nói chung, sử dụng không dấu cho bất cứ điều gì ngoại trừ việc
vặn vẹo

15
@zvrba: Sự khác biệt giữa hai mức lương không phải là tiền lương và do đó, việc sử dụng một loại khác nhau đã được ký là hợp pháp.
JeremyP

12
@JeremyP Có nhưng nếu bạn đang sử dụng C (và có vẻ như điều này cũng đúng trong C ++), phép trừ số nguyên không dấu sẽ dẫn đến một số nguyên không dấu , không thể âm. Nó có thể biến thành giá trị đúng nếu bạn chuyển nó thành int đã ký, nhưng kết quả của phép tính là một số nguyên không dấu. Xem thêm câu trả lời này để biết thêm sự kỳ lạ tính toán có chữ ký / không dấu - đó là lý do tại sao bạn không bao giờ nên sử dụng các biến không dấu trừ khi bạn thực sự vặn vẹo bit.
Tacroy

5
@zvrba: Sự khác biệt là số lượng tiền tệ nhưng không phải là tiền lương. Bây giờ bạn có thể lập luận rằng tiền lương cũng là một đại lượng tiền tệ (bị ràng buộc với số dương và 0 bằng cách xác nhận đầu vào là điều mà hầu hết mọi người sẽ làm) nhưng sự khác biệt giữa hai mức lương không phải là tiền lương.
JeremyP

29

OP không nói gì về loại hệ thống họ đang viết chương trình, nhưng tôi cho rằng OP đã nghĩ đến một PC điển hình có bộ nhớ GB kể từ khi C ++ được đề cập. Như một trong những ý kiến ​​nói, ngay cả với loại bộ nhớ đó, nếu bạn có vài triệu mục thuộc một loại - chẳng hạn như một mảng - thì kích thước của biến có thể tạo ra sự khác biệt.

Nếu bạn tham gia vào thế giới của các hệ thống nhúng - điều không thực sự nằm ngoài phạm vi của câu hỏi, vì OP không giới hạn nó trong PC - thì kích thước của các loại dữ liệu rất nhiều vấn đề. Tôi vừa hoàn thành một dự án nhanh trên một vi điều khiển 8 bit chỉ có 8K từ bộ nhớ chương trình và 368 byte RAM. Ở đó, rõ ràng mỗi byte đều có giá trị. Người ta không bao giờ sử dụng một biến lớn hơn mức họ cần (cả từ quan điểm không gian và kích thước mã - bộ xử lý 8 bit sử dụng rất nhiều hướng dẫn để thao tác dữ liệu 16 và 32 bit). Tại sao sử dụng CPU với nguồn lực hạn chế như vậy? Với số lượng lớn, chúng có thể có giá chỉ bằng một phần tư.

Tôi hiện đang thực hiện một dự án nhúng khác với bộ vi điều khiển dựa trên MIPS 32 bit, có 512K byte flash và 128K byte RAM (và có giá khoảng 6 đô la về số lượng). Như với PC, kích thước dữ liệu "tự nhiên" là 32 bit. Bây giờ nó trở nên hiệu quả hơn, thông minh mã hơn, để sử dụng ints cho hầu hết các biến thay vì ký tự hoặc quần short. Nhưng một lần nữa, bất kỳ loại mảng hoặc cấu trúc nào cũng phải được xem xét liệu các loại dữ liệu nhỏ hơn có được bảo hành hay không. Không giống như trình biên dịch cho các hệ thống lớn hơn, nhiều khả năng các biến trong cấu trúc sẽ được đóng gói trên một hệ thống nhúng. Tôi cẩn thận luôn cố gắng đặt tất cả các biến 32 bit trước, sau đó 16 bit, sau đó 8 bit để tránh bất kỳ "lỗ hổng" nào.


10
+1 cho thực tế là các quy tắc khác nhau áp dụng cho các hệ thống nhúng. Việc C ++ được đề cập không có nghĩa là mục tiêu là PC. Một trong những dự án gần đây của tôi đã được viết bằng C ++ trên bộ xử lý với 32k RAM và 256K Flash.
uɐɪ

13

Câu trả lời phụ thuộc vào hệ thống của bạn. Nói chung, đây là những lợi thế và bất lợi của việc sử dụng các loại nhỏ hơn:

Ưu điểm

  • Các loại nhỏ hơn sử dụng ít bộ nhớ hơn trên hầu hết các hệ thống.
  • Các loại nhỏ hơn cho phép tính nhanh hơn trên một số hệ thống. Đặc biệt đúng với float vs double trên nhiều hệ thống. Và các kiểu int nhỏ hơn cũng cho mã nhanh hơn đáng kể trên CPU 8- hoặc 16 bit.

Nhược điểm

  • Nhiều CPU có yêu cầu căn chỉnh. Một số truy cập dữ liệu liên kết nhanh hơn không được chỉ định. Một số phải có dữ liệu phù hợp để thậm chí có thể truy cập nó. Các kiểu số nguyên lớn hơn bằng một đơn vị căn chỉnh, vì vậy chúng hầu như không được sắp xếp sai. Điều này có nghĩa là trình biên dịch có thể buộc phải đặt các số nguyên nhỏ hơn của bạn thành các số nguyên lớn hơn. Và nếu các loại nhỏ hơn là một phần của cấu trúc lớn hơn, bạn có thể nhận được các byte đệm khác nhau được chèn vào bất kỳ vị trí nào trong cấu trúc bởi trình biên dịch, để sửa lỗi căn chỉnh.
  • Chuyển đổi ngầm nguy hiểm. C và C ++ có một số quy tắc tối nghĩa, nguy hiểm về cách các biến được quảng bá thành các biến lớn hơn, hoàn toàn không có lỗi đánh máy. Có hai bộ quy tắc chuyển đổi ngầm được gắn với nhau, được gọi là "quy tắc quảng cáo số nguyên" và "chuyển đổi số học thông thường". Tìm hiểu thêm về họ ở đây . Các quy tắc này là một trong những nguyên nhân phổ biến nhất gây ra lỗi trong C và C ++. Bạn có thể tránh được rất nhiều vấn đề bằng cách sử dụng cùng một kiểu số nguyên trên toàn bộ chương trình.

Lời khuyên của tôi là thích điều này:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

Ngoài ra, bạn có thể sử dụng int_leastn_thoặc int_fastn_ttừ stdint.h, trong đó n là số 8, 16, 32 hoặc 64. int_leastn_tloại có nghĩa là "Tôi muốn điều này ít nhất là n byte nhưng tôi không quan tâm nếu trình biên dịch phân bổ nó là một loại lớn hơn để phù hợp với căn chỉnh ".

int_fastn_t có nghĩa là "Tôi muốn nó dài n byte, nhưng nếu nó làm cho mã của tôi sẽ chạy nhanh hơn, trình biên dịch nên sử dụng một loại lớn hơn chỉ định".

Nói chung, các loại stdint.h khác nhau thực hành tốt hơn nhiều so với đơn giản int, v.v., vì chúng là hàng xách tay. Mục đích của việc intlà không cung cấp cho nó một chiều rộng được chỉ định để làm cho nó di động. Nhưng trong thực tế, thật khó để chuyển bởi vì bạn không bao giờ biết nó sẽ lớn như thế nào trên một hệ thống cụ thể.


Tại chỗ liên quan đến liên kết. Trong dự án hiện tại của tôi, việc sử dụng uint8_t một cách vô cớ trên MSP430 16 bit đã làm hỏng MCU theo những cách bí ẩn (rất có thể là truy cập bị sai lệch xảy ra ở đâu đó, có lẽ là lỗi của GCC, có lẽ là không) - chỉ thay thế tất cả các uint8_t bằng 'không dấu' đã loại bỏ các sự cố. Sử dụng các loại 8 bit trên các vòm> 8 bit nếu không gây tử vong thì ít nhất là không hiệu quả: trình biên dịch tạo ra các lệnh 'và reg, 0xff' bổ sung. Sử dụng 'int / unsign' cho tính di động và giải phóng trình biên dịch khỏi các ràng buộc bổ sung.
alexei 16/07/2015

11

Tùy thuộc vào cách hệ điều hành cụ thể hoạt động, bạn thường mong muốn bộ nhớ được phân bổ không tối ưu hóa để khi bạn gọi một byte, hoặc một từ hoặc một số loại dữ liệu nhỏ khác được phân bổ, giá trị chiếm toàn bộ đăng ký sở hữu. Cách trình biên dịch hoặc trình thông dịch của bạn hoạt động để diễn giải điều này tuy nhiên là một thứ khác, vì vậy nếu bạn biên dịch một chương trình trong C #, thì giá trị có thể chiếm một thanh ghi cho chính nó, tuy nhiên giá trị sẽ được kiểm tra ranh giới để đảm bảo bạn không cố gắng lưu trữ một giá trị sẽ vượt quá giới hạn của kiểu dữ liệu dự định.

Hiệu suất khôn ngoan và nếu bạn thực sự khoa trương về những thứ như vậy, có thể đơn giản hơn là sử dụng kiểu dữ liệu phù hợp nhất với kích thước thanh ghi mục tiêu, nhưng sau đó bạn bỏ lỡ tất cả các đường cú pháp đáng yêu đó giúp làm việc với các biến dễ dàng .

Điều này giúp bạn như thế nào? Chà, điều đó thực sự tùy thuộc vào bạn để quyết định loại tình huống bạn đang mã hóa. Đối với gần như mọi chương trình tôi từng viết, chỉ cần tin tưởng vào trình biên dịch của bạn để tối ưu hóa mọi thứ và sử dụng kiểu dữ liệu hữu ích nhất cho bạn. Nếu bạn cần độ chính xác cao, hãy sử dụng các kiểu dữ liệu dấu phẩy động lớn hơn. Nếu chỉ làm việc với các giá trị dương, có lẽ bạn có thể sử dụng một số nguyên không dấu, nhưng đối với hầu hết các phần, chỉ cần sử dụng kiểu dữ liệu int là đủ.

Tuy nhiên, nếu bạn có một số yêu cầu dữ liệu rất nghiêm ngặt, chẳng hạn như viết giao thức truyền thông hoặc một loại thuật toán mã hóa nào đó, thì việc sử dụng các kiểu dữ liệu được kiểm tra phạm vi có thể rất hữu ích, đặc biệt nếu bạn đang cố gắng tránh các vấn đề liên quan đến tràn / tràn dữ liệu hoặc giá trị dữ liệu không hợp lệ.

Lý do duy nhất khác mà tôi có thể nghĩ ra khỏi đỉnh đầu để sử dụng các kiểu dữ liệu cụ thể là khi bạn đang cố gắng truyền đạt ý định trong mã của mình. Nếu bạn sử dụng một ví dụ ngắn, bạn đang nói với các nhà phát triển khác rằng bạn đang cho phép các số dương và số âm trong phạm vi giá trị rất nhỏ.


6

Như Scarfridge nhận xét, đây là một

Trường hợp cổ điển của tối ưu hóa sớm .

Cố gắng tối ưu hóa để sử dụng bộ nhớ có thể ảnh hưởng đến các lĩnh vực hiệu suất khác và các quy tắc tối ưu hóa vàng là:

Nguyên tắc tối ưu hóa chương trình đầu tiên: Đừng làm điều đó .

Quy tắc tối ưu hóa chương trình thứ hai (chỉ dành cho chuyên gia!): Đừng làm điều đó . "

- Michael A. Jackson

Để biết liệu bây giờ là thời gian để tối ưu hóa cần phải có điểm chuẩn và thử nghiệm. Bạn cần biết mã của bạn đang ở đâu không hiệu quả, để bạn có thể nhắm mục tiêu tối ưu hóa.

Để xác định xem phiên bản tối ưu hóa của mã thực sự tốt hơn so với triển khai ngây thơ tại bất kỳ thời điểm nào không, bạn cần phải đánh giá chúng song song với cùng một dữ liệu.

Ngoài ra, hãy nhớ rằng chỉ vì một triển khai nhất định có hiệu quả hơn đối với thế hệ CPU hiện tại, không có nghĩa là nó sẽ luôn như vậy. Câu trả lời của tôi cho câu hỏi Tối ưu hóa vi mô có quan trọng khi mã hóa không? chi tiết một ví dụ từ kinh nghiệm cá nhân trong đó tối ưu hóa lỗi thời dẫn đến một mức độ giảm tốc độ.

Trên nhiều bộ xử lý, truy cập bộ nhớ không được phân bổ sẽ tốn kém hơn đáng kể so với truy cập bộ nhớ được căn chỉnh. Đóng gói một vài quần short vào cấu trúc của bạn có thể chỉ có nghĩa là chương trình của bạn phải thực hiện thao tác đóng gói / giải nén mỗi khi bạn chạm vào một trong hai giá trị.

Vì lý do này, trình biên dịch hiện đại bỏ qua đề xuất của bạn. Như ý kiến ​​của nikie :

Với các cài đặt trình biên dịch đóng gói / căn chỉnh tiêu chuẩn, các biến sẽ được căn chỉnh theo ranh giới 4 byte, do đó có thể không có bất kỳ sự khác biệt nào.

Thứ hai đoán trình biên dịch của bạn lúc nguy hiểm.

Có một nơi để tối ưu hóa như vậy, khi làm việc với bộ dữ liệu terabyte hoặc bộ điều khiển vi mô nhúng, nhưng đối với hầu hết chúng ta, nó không thực sự là một mối quan tâm.


3

Sự khác biệt chính là int ngắn chiếm 2 byte bộ nhớ trong khi int chiếm 4 byte và int ngắn có giá trị nhỏ hơn, nhưng chúng ta cũng có thể gọi đây là để làm cho nó nhỏ hơn nữa:

Điều này là không chính xác. Bạn không thể đưa ra các giả định về việc mỗi loại giữ bao nhiêu byte, ngoài charviệc là một byte và ít nhất 8 bit cho mỗi byte, cùng với kích thước của mỗi loại lớn hơn hoặc bằng với trước đó.

Các lợi ích hiệu suất cực kỳ nhỏ cho các biến stack - dù sao chúng cũng có thể được căn chỉnh / đệm.

Bởi vì điều này, shortlongthực tế không sử dụng ngày nay, và bạn hầu như luôn luôn sử dụng tốt hơn int.


Tất nhiên, cũng stdint.hcó thứ hoàn toàn tốt để sử dụng khi intkhông cắt nó. Nếu bạn đã từng phân bổ các mảng số nguyên / cấu trúc khổng lồ thì sẽ hợp intX_tlý khi bạn có thể hiệu quả và dựa vào kích thước của loại. Điều này hoàn toàn không sớm vì bạn có thể tiết kiệm megabyte bộ nhớ.


1
Trên thực tế, với sự ra đời của môi trường 64 bit, longcó thể khác int. Nếu trình biên dịch của bạn là LP64, intlà 32 bit và longlà 64 bit và bạn sẽ thấy đó intvẫn có thể được căn chỉnh 4 byte (ví dụ trình biên dịch của tôi).
JeremyP

1
@JeremyP Vâng, tôi đã nói khác hay sao?
Pubby

Câu cuối cùng của bạn mà tuyên bố ngắn và dài thực tế không sử dụng. Long chắc chắn có một công dụng, nếu chỉ là loại cơ sở củaint64_t
JeremyP

@JeremyP: Bạn có thể sống tốt với int và lâu dài.
gnasher729

@ gnasher729: Bạn sử dụng gì nếu bạn cần một biến có thể chứa các giá trị hơn 65 nghìn, nhưng không bao giờ nhiều như một tỷ? int32_t,int_fast32_tlonglà tất cả các lựa chọn tốt, long longchỉ là lãng phí và intkhông thể mang theo được.
Ben Voigt

3

Điều này sẽ xuất phát từ một loại quan điểm OOP và / hoặc enterprisey / ứng dụng và có thể không áp dụng được trong một số lĩnh vực / lĩnh vực nhất định, nhưng tôi muốn đưa ra khái niệm về nỗi ám ảnh nguyên thủy .

Nên sử dụng các loại dữ liệu khác nhau cho các loại thông tin khác nhau trong ứng dụng của bạn. Tuy nhiên, có lẽ KHÔNG nên sử dụng các loại tích hợp cho việc này, trừ khi bạn có một số vấn đề nghiêm trọng về hiệu suất (đã được đo lường và xác minh, v.v.).

Nếu chúng ta muốn nhiệt độ mô hình trong Kelvin trong ứng dụng của chúng tôi, chúng tôi có thể sử dụng một ushorthoặc uinthoặc một cái gì đó tương tự để biểu thị rằng "khái niệm về độ tiêu cực Kelvin là ngớ ngẩn và một lỗi logic miền". Ý tưởng đằng sau điều này là âm thanh, nhưng bạn sẽ không đi hết con đường này. Điều chúng tôi nhận ra là chúng tôi không thể có các giá trị âm, vì vậy thật tiện lợi nếu chúng tôi có thể có trình biên dịch để đảm bảo không ai gán giá trị âm cho nhiệt độ Kelvin. Điều đó cũng đúng là bạn không thể thực hiện các thao tác bitwise về nhiệt độ. Và bạn không thể thêm số đo trọng lượng (kg) vào nhiệt độ (K). Nhưng nếu bạn mô hình cả nhiệt độ và khối lượng là uints, chúng ta có thể làm điều đó.

Việc sử dụng các loại tích hợp để mô hình hóa các thực thể DOMAIN của chúng tôi bị ràng buộc dẫn đến một số mã lộn xộn và một số kiểm tra bị bỏ lỡ và các bất biến bị hỏng. Ngay cả khi một loại nắm bắt MỘT SỐ phần của thực thể (không thể âm), thì nó vẫn bị bỏ lỡ các phần tử khác (không thể được sử dụng trong các biểu thức số học tùy ý, không thể được coi là một mảng bit, v.v.)

Giải pháp là xác định các loại mới đóng gói các bất biến. Bằng cách này, bạn có thể chắc chắn rằng tiền là tiền và khoảng cách là khoảng cách và bạn không thể cộng chúng lại với nhau và bạn không thể tạo khoảng cách âm, nhưng bạn CÓ THỂ tạo ra một số tiền âm (hoặc nợ). Tất nhiên, các loại này sẽ sử dụng các loại tích hợp bên trong, nhưng đây là bị ẩn khỏi các máy khách. Liên quan đến câu hỏi của bạn về mức tiêu thụ hiệu năng / bộ nhớ, loại điều này có thể cho phép bạn thay đổi cách mọi thứ được lưu trữ bên trong mà không thay đổi giao diện của các chức năng hoạt động trên các thực thể miền của bạn, nếu bạn phát hiện ra rằng, shortthật quá đáng lớn.


1

Phải, tất nhiên. Đó là một ý tưởng tốt để sử dụnguint_least8_t cho từ điển, mảng hằng số lớn, bộ đệm, vv Tốt hơn là sử dụng uint_fast8_tcho mục đích xử lý.

uint8_least_t (lưu trữ) -> uint8_fast_t (xử lý) -> uint8_least_t(lưu trữ).

Ví dụ: bạn đang lấy ký hiệu 8 bit từ source, mã 16 bit dictionariesvà một số 32 bitconstants . Hơn bạn đang xử lý các hoạt động 10-15 bit với chúng và xuất ra 8 bit destination.

Hãy tưởng tượng rằng bạn phải xử lý 2 gigabyte source. Số lượng hoạt động bit là rất lớn. Bạn sẽ nhận được tiền thưởng nước hoa tuyệt vời nếu bạn sẽ chuyển sang loại nhanh trong quá trình xử lý. Các loại nhanh có thể khác nhau cho mỗi họ CPU. Bạn có thể bao gồm stdint.hvà sử dụng uint_fast8_t,uint_fast16_t , uint_fast32_tvv

Bạn có thể sử dụng uint_least8_tthay vì uint8_tcho tính di động. Nhưng không ai thực sự biết những gì cpu hiện đại sẽ sử dụng tính năng này. Máy VAC là một mảnh bảo tàng. Vì vậy, có thể nó là một quá mức cần thiết.


1
Mặc dù bạn có thể có một điểm với các loại dữ liệu bạn đã liệt kê, bạn nên giải thích lý do tại sao chúng tốt hơn thay vì chỉ nói rằng đó là. Đối với những người như tôi không quen thuộc với các loại dữ liệu đó, tôi đã phải google chúng để hiểu những gì bạn đang nói.
Peter M
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.