Mengapa sizeof untuk struct tidak sama dengan jumlah sizeof masing-masing anggota?


697

Mengapa sizeofoperator mengembalikan ukuran yang lebih besar untuk struktur daripada ukuran total anggota struktur?


14
Lihat FAQ C ini tentang peningkatan memori. c-faq.com/struct/align.esr.html
Richard Chambers

48
Anekdot: Ada virus komputer aktual yang meletakkan kodenya di dalam pad struct di program host.
Elazar

4
@ Elazar Itu mengesankan! Saya tidak akan pernah berpikir mungkin untuk menggunakan area sekecil itu untuk apa pun. Apakah Anda dapat memberikan detail lebih lanjut?
Wilson

1
@ Willson - Saya yakin ini melibatkan banyak jmp.
hoodaticus

4
Lihat struktur bantalan, pengepakan : Seni Struktur C yang Hilang Pengepakan Eric S. Raymond
EsmaeelE

Jawaban:


649

Ini karena bantalan ditambahkan untuk memenuhi kendala penyelarasan. Penyelarasan struktur data berdampak pada kinerja dan kebenaran program:

  • Akses yang tidak selaras mungkin merupakan kesalahan yang sulit (sering SIGBUS).
  • Akses yang tidak selaras mungkin merupakan kesalahan lunak.
    • Baik dikoreksi dalam perangkat keras, untuk penurunan kinerja yang sederhana.
    • Atau dikoreksi oleh persaingan dalam perangkat lunak, untuk penurunan kinerja yang parah.
    • Selain itu, atomisitas dan jaminan konkurensi lainnya mungkin rusak, yang menyebabkan kesalahan halus.

Berikut adalah contoh menggunakan pengaturan khas untuk prosesor x86 (semua mode 32 dan 64 bit yang digunakan):

struct X
{
    short s; /* 2 bytes */
             /* 2 padding bytes */
    int   i; /* 4 bytes */
    char  c; /* 1 byte */
             /* 3 padding bytes */
};

struct Y
{
    int   i; /* 4 bytes */
    char  c; /* 1 byte */
             /* 1 padding byte */
    short s; /* 2 bytes */
};

struct Z
{
    int   i; /* 4 bytes */
    short s; /* 2 bytes */
    char  c; /* 1 byte */
             /* 1 padding byte */
};

const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */

Seseorang dapat meminimalkan ukuran struktur dengan menyortir anggota dengan menyelaraskan (menyortir berdasarkan ukuran sudah cukup untuk itu dalam tipe dasar) (seperti struktur Zdalam contoh di atas).

CATATAN PENTING: Standar C dan C ++ menyatakan bahwa penyelarasan struktur ditentukan oleh implementasi. Oleh karena itu setiap kompiler dapat memilih untuk menyelaraskan data secara berbeda, menghasilkan tata letak data yang berbeda dan tidak kompatibel. Untuk alasan ini, ketika berhadapan dengan perpustakaan yang akan digunakan oleh kompiler yang berbeda, penting untuk memahami bagaimana kompiler menyelaraskan data. Beberapa kompiler memiliki pengaturan baris perintah dan / atau #pragmapernyataan khusus untuk mengubah pengaturan penyelarasan struktur.


38
Saya ingin membuat catatan di sini: Kebanyakan prosesor menghukum Anda karena akses memori yang tidak selaras (seperti yang Anda sebutkan), tetapi Anda tidak dapat melupakan bahwa banyak yang sepenuhnya tidak mengizinkannya. Sebagian besar chip MIPS, khususnya, akan mengeluarkan pengecualian pada akses yang tidak selaras.
Cody Brocious

35
Chip x86 sebenarnya agak unik karena memungkinkan akses yang tidak selaras, meskipun dikenakan sanksi; Kebanyakan chip AFAIK akan melempar pengecualian, tidak hanya beberapa. PowerPC adalah contoh umum lainnya.
Dark Shikari

6
Mengaktifkan pragma untuk akses yang tidak selaras umumnya menyebabkan kode Anda membesar, pada prosesor yang melempar kesalahan misalignment, karena kode untuk memperbaiki setiap misalignment harus dihasilkan. ARM juga melempar kesalahan misalignment.
Mike Dimmick

5
@ Gelap - sepenuhnya setuju. Tetapi kebanyakan prosesor desktop adalah x86 / x64, jadi sebagian besar chip tidak mengeluarkan kesalahan penyelarasan data;)
Aaron

27
Akses data yang tidak selaras biasanya merupakan fitur yang ditemukan dalam arsitektur CISC, dan sebagian besar arsitektur RISC tidak memasukkannya (ARM, MIPS, PowerPC, Cell). Sebenarnya, sebagian besar chip BUKAN prosesor desktop, untuk aturan tertanam oleh jumlah chip dan sebagian besar adalah arsitektur RISC.
Lara Dougan

191

Pengemasan dan perataan byte, seperti dijelaskan dalam FAQ C di sini :

Ini untuk penyelarasan. Banyak prosesor tidak dapat mengakses kuantitas 2- dan 4-byte (mis. Int dan int panjang) jika mereka dijejalkan dalam segala hal.

Misalkan Anda memiliki struktur ini:

struct {
    char a[3];
    short int b;
    long int c;
    char d[3];
};

Sekarang, Anda mungkin berpikir bahwa mungkin untuk mengemas struktur ini ke dalam memori seperti ini:

+-------+-------+-------+-------+
|           a           |   b   |
+-------+-------+-------+-------+
|   b   |           c           |
+-------+-------+-------+-------+
|   c   |           d           |
+-------+-------+-------+-------+

Tetapi jauh lebih mudah pada prosesor jika kompiler mengaturnya seperti ini:

+-------+-------+-------+
|           a           |
+-------+-------+-------+
|       b       |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           |
+-------+-------+-------+

Dalam versi dikemas, perhatikan bagaimana setidaknya sedikit sulit bagi Anda dan saya untuk melihat bagaimana bidang b dan c membungkus? Singkatnya, sulit untuk prosesor juga. Oleh karena itu, sebagian besar penyusun akan memasang struktur (seolah-olah dengan bidang ekstra dan tak terlihat) seperti ini:

+-------+-------+-------+-------+
|           a           | pad1  |
+-------+-------+-------+-------+
|       b       |     pad2      |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           | pad3  |
+-------+-------+-------+-------+

1
Sekarang apa gunanya slot memori pad1, pad2 dan pad3.
Lakshmi Sreekanth Chitla


@EmmEff ini mungkin salah tetapi saya tidak mengerti: mengapa tidak ada slot memori untuk pointer di array?
Balázs Börcsök

1
@ BalázsBörcsök Ini adalah array ukuran konstan, dan elemen-elemennya disimpan langsung dalam struct pada offset tetap. Kompiler mengetahui semua ini pada waktu kompilasi sehingga pointer tersirat. Sebagai contoh, jika Anda memiliki variabel struct dari jenis ini dipanggil smaka &s.a == &sdan &s.d == &s + 12(diberi perataan yang ditunjukkan dalam jawaban) Pointer hanya disimpan jika array memiliki ukuran variabel (misalnya, adideklarasikan char a[]sebagai ganti char a[3]), tetapi kemudian elemen harus disimpan di tempat lain.
kbolino

27

Jika Anda ingin struktur memiliki ukuran tertentu dengan GCC misalnya gunakan __attribute__((packed)).

Pada Windows Anda dapat mengatur perataan ke satu byte saat menggunakan compier cl.exe dengan opsi / Zp .

Biasanya lebih mudah bagi CPU untuk mengakses data yang merupakan kelipatan dari 4 (atau 8), tergantung platform dan juga pada kompiler.

Jadi pada dasarnya ini adalah masalah penyelarasan.

Anda harus punya alasan kuat untuk mengubahnya.


5
"alasan bagus" Contoh: Menjaga kompatibilitas biner (padding) konsisten antara sistem 32-bit dan 64-bit untuk struct yang kompleks dalam kode demo pembuktian konsep yang dipamerkan besok. Terkadang keharusan harus diutamakan daripada kesopanan.
Mr.Ree

2
Semuanya baik-baik saja kecuali ketika Anda menyebutkan Sistem Operasi. Ini adalah masalah untuk kecepatan CPU, OS tidak terlibat sama sekali.
Blaisorblade

3
Alasan bagus lainnya adalah jika Anda memasukkan datastream ke dalam sebuah struct, misalnya saat menguraikan protokol jaringan.
ceo

1
@dolmen, saya baru saja mengatakan bahwa "lebih mudah bagi Sistem Operatin untuk mengakses data" salah, karena OS tidak mengakses data.
Blaisorblade

1
@dolmen Sebenarnya, seseorang harus berbicara tentang ABI (aplikasi binary interface). Penyelarasan default (digunakan jika Anda tidak mengubahnya di sumber) tergantung pada ABI, dan banyak OS mendukung beberapa ABI (katakanlah, 32- dan 64-bit, atau untuk binari dari OS yang berbeda, atau untuk berbagai cara kompilasi binari yang sama untuk OS yang sama). OTOH, keselarasan apa yang nyaman menurut kinerja tergantung pada CPU - memori diakses dengan cara yang sama apakah Anda menggunakan mode 32 atau 64 bit (Saya tidak dapat mengomentari mode nyata, tetapi tampaknya hampir tidak relevan untuk kinerja saat ini). IIRC Pentium mulai lebih memilih perataan 8-byte.
Blaisorblade

15

Ini bisa disebabkan oleh perataan byte dan padding sehingga struktur keluar hingga jumlah byte (atau kata-kata) yang merata pada platform Anda. Misalnya dalam C di Linux, 3 struktur berikut:

#include "stdio.h"


struct oneInt {
  int x;
};

struct twoInts {
  int x;
  int y;
};

struct someBits {
  int x:2;
  int y:6;
};


int main (int argc, char** argv) {
  printf("oneInt=%zu\n",sizeof(struct oneInt));
  printf("twoInts=%zu\n",sizeof(struct twoInts));
  printf("someBits=%zu\n",sizeof(struct someBits));
  return 0;
}

Memiliki anggota yang ukurannya (dalam byte) masing-masing adalah 4 byte (32 bit), 8 byte (2x 32 bit) dan 1 byte (2 + 6 bit). Program di atas (di Linux menggunakan gcc) mencetak ukuran 4, 8, dan 4 - di mana struktur terakhir diisi sehingga menjadi satu kata (4 x 8 bit byte pada platform 32bit saya).

oneInt=4
twoInts=8
someBits=4

4
"C di Linux menggunakan gcc" tidak cukup untuk menggambarkan platform Anda. Alignment sebagian besar tergantung pada arsitektur CPU.
dolmen

- @ Kyle Burton. Maaf, saya tidak mengerti mengapa ukuran struktur "someBits" sama dengan 4, saya berharap 8 byte karena ada 2 bilangan bulat yang dinyatakan (2 * sizeof (int)) = 8 byte. terima kasih
youpilat13

1
Hai @ youpilat13, :2dan :6sebenarnya menentukan 2 dan 6 bit, bukan bilangan bulat 32 bit penuh dalam kasus ini. someBits.x, karena hanya 2 bit hanya dapat menyimpan 4 nilai yang mungkin: 00, 01, 10, dan 11 (1, 2, 3 dan 4). Apakah ini masuk akal? Inilah artikel tentang fitur ini: geeksforgeeks.org/bit-fields-c
Kyle Burton

11

Lihat juga:

untuk Microsoft Visual C:

http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx

dan kompatibilitas klaim GCC dengan kompiler Microsoft .:

http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html

Selain jawaban sebelumnya, harap perhatikan bahwa terlepas dari kemasannya, tidak ada jaminan pesanan anggota di C ++ . Compiler dapat (dan tentu saja melakukannya) menambahkan pointer tabel virtual dan anggota struktur dasar ke struktur. Bahkan keberadaan tabel virtual tidak dijamin oleh standar (implementasi mekanisme virtual tidak ditentukan) dan oleh karena itu orang dapat menyimpulkan bahwa jaminan tersebut tidak mungkin.

Saya yakin anggota-order yang dijamin dalam C , tapi aku tidak akan mengandalkan itu, ketika menulis sebuah cross-platform atau cross-compiler Program.


4
"Saya cukup yakin pesanan anggota didengus dalam C". Ya, C99 mengatakan: "Dalam objek struktur, anggota non-bit-field dan unit di mana bit-field berada memiliki alamat yang meningkat sesuai dengan urutannya." Lebih banyak kebaikan standar di: stackoverflow.com/a/37032302/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 事件


8

Ukuran struktur lebih besar daripada jumlah bagian-bagiannya karena apa yang disebut pengepakan. Prosesor tertentu memiliki ukuran data yang disukai yang bekerja dengannya. Ukuran prosesor paling modern lebih disukai jika 32-bit (4 byte). Mengakses memori ketika data pada batas jenis ini lebih efisien daripada hal-hal yang mengangkangi batas ukuran itu.

Sebagai contoh. Pertimbangkan struktur sederhana:

struct myStruct
{
   int a;
   char b;
   int c;
} data;

Jika mesin adalah mesin 32-bit dan data disejajarkan pada batas 32-bit, kami melihat masalah langsung (dengan asumsi tidak ada struktur keselarasan). Dalam contoh ini, mari kita asumsikan bahwa data struktur dimulai pada alamat 1024 (0x400 - perhatikan bahwa 2 bit terendah adalah nol, sehingga data disejajarkan dengan batas 32-bit). Akses ke data.a akan berfungsi dengan baik karena dimulai pada batas - 0x400. Akses ke data.b juga akan berfungsi dengan baik, karena berada di alamat 0x404 - batas 32-bit lainnya. Tetapi struktur yang tidak selaras akan menempatkan data.c di alamat 0x405. 4 byte data.c berada di 0x405, 0x406, 0x407, 0x408. Pada mesin 32-bit, sistem akan membaca data.c selama satu siklus memori, tetapi hanya akan mendapatkan 3 dari 4 byte (byte ke-4 berada pada batas berikutnya). Jadi, sistem harus melakukan akses memori kedua untuk mendapatkan byte ke-4,

Sekarang, jika alih-alih meletakkan data.c di alamat 0x405, kompiler mengisi struktur dengan 3 byte dan meletakkan data.c di alamat 0x408, maka sistem hanya perlu 1 siklus untuk membaca data, memotong waktu akses ke elemen data tersebut sebesar 50%. Padding menukar efisiensi memori untuk efisiensi pemrosesan. Mengingat bahwa komputer dapat memiliki jumlah memori yang sangat besar (banyak gigabytes), kompiler merasa bahwa swap (kecepatan lebih dari ukuran) adalah wajar.

Sayangnya, masalah ini menjadi pembunuh ketika Anda mencoba mengirim struktur melalui jaringan atau bahkan menulis data biner ke file biner. Padding yang disisipkan di antara elemen struktur atau kelas dapat mengganggu data yang dikirim ke file atau jaringan. Untuk menulis kode portabel (yang akan menuju ke beberapa kompiler berbeda), Anda mungkin harus mengakses setiap elemen struktur secara terpisah untuk memastikan "pengepakan" yang tepat.

Di sisi lain, kompiler yang berbeda memiliki kemampuan berbeda untuk mengelola pengemasan struktur data. Misalnya, dalam Visual C / C ++ kompiler mendukung perintah paket #pragma. Ini akan memungkinkan Anda untuk menyesuaikan pengemasan dan penyelarasan data.

Sebagai contoh:

#pragma pack 1
struct MyStruct
{
    int a;
    char b;
    int c;
    short d;
} myData;

I = sizeof(myData);

Saya sekarang harus memiliki panjang 11. Tanpa pragma, saya bisa apa saja dari 11 hingga 14 (dan untuk beberapa sistem, sebanyak 32), tergantung pada kemasan default dari kompiler.


Ini membahas konsekuensi padding struktur, tetapi tidak menjawab pertanyaan.
Keith Thompson

" ... karena apa yang disebut pengepakan. ... - Saya pikir maksud Anda" padding "." Ukuran prosesor paling modern lebih disukai jika 32-bit (4 byte) "- Itu sedikit penyederhanaan yang berlebihan. Biasanya ukuran 8, 16, 32, dan 64 bit didukung, seringkali setiap ukuran memiliki penyelarasannya sendiri. Dan saya tidak yakin jawaban Anda menambahkan informasi baru yang belum ada dalam jawaban yang diterima.
Keith Thompson

1
Ketika saya mengatakan pengepakan, saya maksudkan bagaimana kompiler mengemas data ke dalam suatu struktur (dan itu bisa dilakukan dengan mengisi item-item kecil, tetapi tidak perlu membalut, tetapi selalu mengemasnya). Adapun ukuran - saya berbicara tentang arsitektur sistem, bukan apa sistem akan mendukung untuk akses data (yang jauh berbeda dari arsitektur bus yang mendasarinya). Adapun komentar terakhir Anda, saya memberikan penjelasan yang disederhanakan dan diperluas dari satu aspek tradeoff (kecepatan versus ukuran) - masalah pemrograman utama. Saya juga menjelaskan cara untuk memperbaiki masalah - itu tidak ada dalam jawaban yang diterima.
sid1138

"Pengepakan" dalam konteks ini biasanya mengacu pada pengalokasian anggota yang lebih ketat daripada standarnya, seperti pada #pragma pack. Jika anggota dialokasikan pada penyelarasan default mereka, saya biasanya mengatakan struktur tidak dikemas.
Keith Thompson

Packing adalah jenis istilah yang kelebihan beban. Ini berarti bagaimana Anda memasukkan elemen struktur ke dalam memori. Mirip dengan makna memasukkan benda ke dalam kotak (packing untuk bergerak). Ini juga berarti memasukkan elemen ke dalam memori tanpa bantalan (semacam tangan pendek untuk "penuh sesak"). Lalu ada versi perintah kata dalam perintah paket #pragma.
sid1138

5

Ini dapat dilakukan jika Anda secara implisit atau eksplisit mengatur penyelarasan struct. Sebuah struct yang disejajarkan 4 akan selalu menjadi kelipatan dari 4 byte bahkan jika ukuran anggotanya akan menjadi sesuatu yang bukan kelipatan dari 4 byte.

Juga sebuah perpustakaan dapat dikompilasi di bawah x86 dengan int 32-bit dan Anda dapat membandingkan komponen-komponennya pada proses 64-bit akan memberi Anda hasil yang berbeda jika Anda melakukannya dengan tangan.


5

C99 N1256 draft standar

http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

6.5.3.4 Ukuran operator :

3 Ketika diterapkan pada operan yang memiliki tipe struktur atau gabungan, hasilnya adalah jumlah total byte dalam objek tersebut, termasuk padding internal dan trailing.

6.7.2.1 Penentu struktur dan serikat :

13 ... Mungkin ada bantalan yang tidak disebutkan namanya dalam objek struktur, tetapi tidak pada awalnya.

dan:

15 Mungkin ada bantalan yang tidak disebutkan namanya di ujung struktur atau gabungan.

Fitur anggota array fleksibel C99 baru ( struct S {int is[];};) juga dapat mempengaruhi bantalan:

16 Sebagai kasus khusus, elemen terakhir dari suatu struktur dengan lebih dari satu anggota bernama dapat memiliki tipe array yang tidak lengkap; ini disebut anggota array yang fleksibel. Dalam sebagian besar situasi, anggota array fleksibel diabaikan. Secara khusus, ukuran struktur adalah seolah-olah anggota larik fleksibel dihilangkan kecuali bahwa itu mungkin memiliki lebih banyak jejak daripada yang akan disiratkan kelalaian.

Lampiran J Masalah Portabilitas mengulangi:

Berikut ini tidak ditentukan: ...

  • Nilai padding byte saat menyimpan nilai dalam struktur atau serikat pekerja (6.2.6.1)

C ++ 11 N3337 konsep standar

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

5.3.3 Ukuran :

2 Ketika diterapkan pada suatu kelas, hasilnya adalah jumlah byte dalam suatu objek dari kelas itu termasuk setiap bantalan yang diperlukan untuk menempatkan objek jenis itu dalam sebuah array.

9.2 Anggota kelas :

Sebuah penunjuk ke objek struct tata letak standar, yang sesuai dikonversi menggunakan reinterpret_cast, menunjuk ke anggota awalnya (atau jika anggota itu adalah bidang-bit, kemudian ke unit di mana ia berada) dan sebaliknya. [Catatan: Karena itu mungkin ada padding tanpa nama di dalam objek struct tata letak standar, tetapi tidak pada awalnya, yang diperlukan untuk mencapai penyelarasan yang tepat. - catatan akhir]

Saya hanya tahu cukup C ++ untuk memahami catatan :-)


4

Selain jawaban lain, sebuah struct dapat (tetapi biasanya tidak) memiliki fungsi virtual, dalam hal ini ukuran dari struct juga akan mencakup ruang untuk vtbl.


8
Tidak terlalu. Dalam implementasi tipikal, apa yang ditambahkan ke struct adalah pointer vtable .
Don Wakefield

3

Bahasa C membuat kompiler memiliki kebebasan tentang lokasi elemen struktural dalam memori:

  • lubang memori dapat muncul di antara dua komponen, dan setelah komponen terakhir. Itu karena fakta bahwa beberapa jenis objek pada komputer target mungkin dibatasi oleh batas pengalamatan
  • "lubang memori" ukuran termasuk dalam hasil sizeof operator. Sizeof saja tidak termasuk ukuran array fleksibel, yang tersedia di C / C ++
  • Beberapa implementasi bahasa memungkinkan Anda untuk mengontrol tata letak memori struktur melalui opsi pragma dan kompiler

Bahasa C memberikan jaminan kepada programmer tentang tata letak elemen dalam struktur:

  • kompiler diperlukan untuk menetapkan urutan komponen yang meningkatkan alamat memori
  • Alamat komponen pertama bertepatan dengan alamat awal struktur
  • bidang bit yang tidak disebutkan namanya dapat dimasukkan dalam struktur ke perataan alamat yang diperlukan dari elemen yang berdekatan

Masalah yang terkait dengan penyelarasan elemen:

  • Komputer yang berbeda melapisi tepi objek dengan cara yang berbeda
  • Pembatasan yang berbeda pada lebar bidang bit
  • Komputer berbeda tentang cara menyimpan byte dalam kata (Intel 80x86 dan Motorola 68000)

Cara kerja pelurusan:

  • Volume yang ditempati oleh struktur dihitung sebagai ukuran elemen tunggal yang disejajarkan dari array struktur tersebut. Struktur harus diakhiri sehingga elemen pertama dari struktur berikut berikutnya tidak melanggar persyaratan penyelarasan

ps. Informasi lebih rinci tersedia di sini: "Referensi Samuel P. Harbison, Guy L.Steele CA, (5.6.2 - 5.6.7)"


2

Idenya adalah untuk pertimbangan kecepatan dan cache, operan harus dibaca dari alamat yang disesuaikan dengan ukuran alami mereka. Untuk membuat ini terjadi, kompiler bantalan struktur anggota sehingga anggota berikut atau struct berikut akan disejajarkan.

struct pixel {
    unsigned char red;   // 0
    unsigned char green; // 1
    unsigned int alpha;  // 4 (gotta skip to an aligned offset)
    unsigned char blue;  // 8 (then skip 9 10 11)
};

// next offset: 12

Arsitektur x86 selalu dapat mengambil alamat yang tidak selaras. Namun, itu lebih lambat dan ketika misalignment tumpang tindih dua garis cache yang berbeda, maka itu akan membatalkan dua baris cache ketika akses yang selaras hanya akan mengusir satu.

Beberapa arsitektur benar-benar harus menjebak membaca dan menulis yang tidak selaras, dan versi awal arsitektur ARM (yang berevolusi menjadi semua CPU seluler saat ini) ... well, mereka sebenarnya baru saja mengembalikan data buruk untuk itu. (Mereka mengabaikan bit orde rendah.)

Akhirnya, perhatikan bahwa garis cache bisa menjadi besar secara sewenang-wenang, dan kompiler tidak berusaha menebaknya atau membuat tradeoff ruang-vs-kecepatan. Sebaliknya, keputusan penyelarasan adalah bagian dari ABI dan mewakili penyelarasan minimum yang pada akhirnya akan secara merata mengisi garis cache.

TL; DR: penyelarasan itu penting.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.