Varlık Sistemlerinde önbellek kaçırma ve kullanılabilirlik


18

Son zamanlarda çerçevem ​​için bir Varlık Sistemi araştırıyor ve uyguluyorum. Sanırım bulabildiğim birçok makale, reddits ve soruları okudum ve şimdiye kadar fikri yeterince iyi kavradığımı düşünüyorum.

Ancak, genel C ++ davranışı, varlık sistemini uyguladığım dil ve bazı kullanılabilirlik sorunları hakkında bazı sorular ortaya attı.

Bu nedenle, bir yaklaşım, varlıklar içinde bir dizi bileşeni doğrudan depolamak olacaktır, ki ben yapmadım çünkü veri üzerinden yineleme yaparken önbellek yerini mahveder. Bu nedenle, bileşen türü başına bir dizi olmaya karar verdim, bu nedenle aynı türdeki tüm bileşenler bellekte bitişiktir, bu da hızlı yineleme için en uygun çözüm olmalıdır.

Ancak, gerçek bir oyun uygulamasında bir sistemden onlarla bir şeyler yapmak için bileşen dizilerini yinelediğimde, neredeyse her zaman iki veya daha fazla bileşen türüyle aynı anda çalıştığımı fark ettim. Örneğin, oluşturma sistemi gerçekte bir render çağrısı yapmak için Transform ve Model bileşenlerini birlikte kullanır. Benim sorum şu, bu gibi durumlarda bir seferde doğrusal olarak bitişik bir diziyi yinelemediğim için, bileşenleri bu şekilde ayırmaktan elde edilen performans kazanımlarını hemen feda ediyor muyum? C ++ 'da iki farklı bitişik diziyi yinelediğimde ve her döngüden her ikisinden veri kullandığımda bir sorun mu var?

Sormak istediğim başka bir şey, bileşenlerin veya varlıklara referansları nasıl tutması gerektiğidir, çünkü bileşenlerin bellekte nasıl yerleştirildiğinin doğası, dizideki konumları kolayca değiştirebilir veya dizi genişletmek için yeniden tahsis edilebilir veya küçülüyor, bileşen işaretçilerimi veya tanıtıcılarımı geçersiz bırakıyor. Sık sık kendimi her karede dönüşümler ve diğer bileşenler üzerinde çalışmak istediğimi bulduğum ve tutamaçlarım veya işaretçilerim geçersizse, her kareyi aramak oldukça dağınık olduğundan bu durumları nasıl ele almanızı öneririm.


4
Ben bileşenleri sürekli bir bellek içine koyarak rahatsız olmaz ama sadece dinamik olarak her bileşen için bellek ayırmak. Bitişik bellek, olasılıkla herhangi bir önbellek performansı artışı sağlar, çünkü bileşenlere zaten oldukça rasgele sırada erişebilirsiniz.
JarkkoL

@Grimshaw İşte okumak için ilginç bir makale: harmful.cat-v.org/software/OO_programming/_pdf/…
Raxvan

@JarkkoL -10 puan. Sistem önbelleği dostu bir şekilde oluşturursanız ve rasgele bir şekilde erişirseniz, performans gerçekten acıyor , sadece sesiyle aptalca. Doğrusal yoldan erişim noktası . ECS ve performans kazancı sanatı, erişilen C / S'yi doğrusal bir şekilde yazmakla ilgilidir.
wondra

@Grimshaw, önbelleğin bir tamsayıdan daha büyük olduğunu unutmayın. Kullanılabilir birkaç KB L1 önbelleği (ve diğer MB'ları) var, eğer korkunç bir şey yapmazsanız, aynı anda ve önbellek dostu olarak birkaç sisteme erişmek tamam olmalıdır.
wondra

2
@wondra Bileşenlere doğrusal erişimi nasıl sağlarsınız? Görüntü oluşturma için bileşenler toplarsam ve varlıkların kameradan azalan sırada işlenmesini istersem diyelim. Bu varlıkların oluşturma bileşenlerine bellekte doğrusal olarak erişilmez. Söyledikleriniz teoride güzel bir şey olsa da, pratikte çalıştığını görmüyorum, ancak beni yanlış kanıtlarsanız sevindim (:
JarkkoL

Yanıtlar:


13

İlk olarak, bu durumda kullanım durumunuza bağlı olarak çok erken optimizasyon yaptığınızı söylemem. Her halükarda, ilginç bir soru sordunuz ve bununla ilgili deneyimim olduğu için, tartacağım. Sadece bir şeyleri nasıl yaptığımı ve yolda ne bulduğumu açıklamaya çalışacağım.

  • Her varlık, herhangi bir türü temsil edebilecek bir genel bileşen tanıtıcıları vektörü içerir.
  • Her bileşen tutamacı, ham bir T * işaretçisi elde etmek için kaydı kaldırılabilir. *Aşağıya bakınız.
  • Her bileşen türünün kendi havuzu, sürekli bir bellek bloğu vardır (benim durumumda sabit boyut).

Hayır, sadece bir bileşen havuzunu her zaman geçemeyeceğiniz ve ideal, temiz şeyi yapamayacağınız belirtilmelidir. Söylediğiniz gibi, bileşenler arasında kaçınılmaz bağlantılar vardır, burada şeyleri bir anda işlemeniz gerekir.

Ancak, gerçekte belirli bir bileşen türü için bir for döngüsü yazabilir ve CPU önbellek hatlarınızdan büyük ölçüde yararlanabileceğiniz durumlar (bulduğum gibi) vardır. Farkında olmayan veya daha fazla bilgi edinmek isteyenler için https://en.wikipedia.org/wiki/Locality_of_reference adresine bakın . Aynı notta, mümkünse, bileşen boyutunuzu CPU önbellek hattı boyutunuzdan küçük veya ona eşit tutmaya çalışın. Satır boyutum 64 bayttı, ki yaygın olduğuna inanıyorum.

Benim durumumda, sistemi uygulamak için çaba harcamak buna değdi. Görünür performans artışları gördüm (elbette profilli). Bunun iyi bir fikir olup olmadığına kendiniz karar vermeniz gerekecek. 1000+ birimde gördüğüm en büyük performans artışı.

Sormak istediğim başka bir şey, bileşenlerin veya varlıklara referansları nasıl tutması gerektiğidir, çünkü bileşenlerin bellekte nasıl yerleştirildiğinin doğası, dizideki konumları kolayca değiştirebilir veya dizi genişletmek için yeniden tahsis edilebilir veya küçülüyor, bileşen işaretçilerimi veya tanıtıcılarımı geçersiz bırakıyor. Sık sık kendimi her karede dönüşümler ve diğer bileşenler üzerinde çalışmak istediğimi bulduğum ve tutamaçlarım veya işaretçilerim geçersizse, her kareyi aramak oldukça dağınık olduğundan bu durumları nasıl ele almanızı öneririm.

Ayrıca bu sorunu bizzat çözdüm. Ben bir sistem sahip sona erdi nerede:

  • Her bileşen tanıtıcısı bir havuz dizinine başvuru içerir
  • Bir bileşen bir havuzdan 'silindiğinde' veya 'kaldırıldığında', bu havuzdaki son bileşen (tam anlamıyla std :: move ile) şimdi boş konuma taşınır veya son bileşeni sildiyseniz hiçbiri taşınır.
  • Bir 'takas' meydana geldiğinde, herhangi bir dinleyiciyi bildiren bir geri çağırma var, böylece herhangi bir somut işaretleyiciyi (örneğin T *) güncelleyebilirler.

* Her zaman yüksek kullanım kodunun belirli bölümlerinde, ele aldığım varlıkların sayısı ile bileşen tutamaçlarını her zaman serbest bırakmaya çalışmanın bir performans sorunu olduğunu buldum. Bu nedenle, şimdi projemin performans açısından kritik kısımlarında bazı ham T işaretçileri koruyorum, ancak aksi takdirde mümkün olduğunda kullanılması gereken genel bileşen tutamaçlarını kullanıyorum. Bunları yukarıda belirtildiği gibi, geri çağrı sistemi ile geçerli tutarım. Bu kadar ileri gitmenize gerek olmayabilir.

Her şeyden önce, sadece bir şeyler deneyin. Gerçek bir dünya senaryosu elde edene kadar, burada kimsenin söylediği her şey sizin için uygun olmayabilir.

Bu yardımcı olur mu? Belirsiz olan her şeyi açıklığa kavuşturmaya çalışacağım. Ayrıca herhangi bir düzeltme takdir edilmektedir.


Bu, gerçekten iyi bir cevaptı ve gümüş bir kurşun olmasa da, birisinin benzer tasarım fikirlerine sahip olduğunu görmek hala iyi. ES'mde de bazı püf noktalarım var ve pratik görünüyorlar. Çok teşekkürler! Eğer ortaya çıkarlarsa başka fikirlere yorum yapmaktan çekinmeyin.
Grimshaw

5

Sadece buna cevap vermek için:

Benim sorum şu, bu gibi durumlarda bir seferde doğrusal olarak bitişik bir diziyi yinelemediğim için, bileşenleri bu şekilde ayırmaktan elde edilen performans kazanımlarını hemen feda ediyor muyum? C ++ 'da iki farklı bitişik dizi yinelediğimde ve her döngüden her ikisinden veri kullandığımda bir sorun mu var?

Hayır (en azından zorunlu olarak). Önbellek denetleyicisi, çoğu durumda, birden fazla bitişik diziden okuma ile etkin bir şekilde başa çıkabilmelidir. Önemli olan, her diziye doğrusal olarak erişmek için mümkün olan her yerde denemektir.

Bunu göstermek için küçük bir karşılaştırma ölçütü yazdım (normal karşılaştırma uyarıları geçerlidir).

Basit bir vektör yapısı ile başlayarak:

struct float3 { float x, y, z; };

İki ayrı dizinin her elemanını toplayan ve üçüncüsünde sonucu saklayan bir döngünün, kaynak verilerin tek bir dizide araya sokulduğu ve sonucun üçte birinde saklandığı bir versiyonla tam olarak aynı şekilde gerçekleştirildiğini buldum. Ancak, sonucu kaynağa eklemiş olsaydım performans düştü (yaklaşık 2 faktör).

Verilere rastgele erişirsem, performans 10 ila 20 arasında bir faktörden muzdaripti.

Zamanlamalar (10.000.000 öğe)

doğrusal erişim

  • ayrı diziler 0.21s
  • serpiştirilmiş kaynak 0.21s
  • serpiştirilmiş kaynak ve sonuç 0.48s

rastgele erişim (rastgele rastgele_Karıştır)

  • ayrı diziler 2.42s
  • serpiştirilmiş kaynak 4.43s
  • serpiştirilmiş kaynak ve sonuç 4.00s

Kaynak (Visual Studio 2013 ile derlenmiştir):

#include <Windows.h>
#include <vector>
#include <algorithm>
#include <iostream>

struct float3 { float x, y, z; };

float3 operator+( float3 const &a, float3 const &b )
{
    return float3{ a.x + b.x, a.y + b.y, a.z + b.z };
}

struct Both { float3 a, b; };

struct All { float3 a, b, res; };


// A version without any indirection
void sum( float3 *a, float3 *b, float3 *res, int n )
{
    for( int i = 0; i < n; ++i )
        *res++ = *a++ + *b++;
}

void sum( float3 *a, float3 *b, float3 *res, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        res[*index] = a[*index] + b[*index];
}

void sum( Both *both, float3 *res, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        res[*index] = both[*index].a + both[*index].b;
}

void sum( All *all, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        all[*index].res = all[*index].a + all[*index].b;
}

class PerformanceTimer
{
public:
    PerformanceTimer() { QueryPerformanceCounter( &start ); }
    double time()
    {
        LARGE_INTEGER now, freq;
        QueryPerformanceCounter( &now );
        QueryPerformanceFrequency( &freq );
        return double( now.QuadPart - start.QuadPart ) / double( freq.QuadPart );
    }
private:
    LARGE_INTEGER start;
};

int main( int argc, char* argv[] )
{
    const int count = 10000000;

    std::vector< float3 > a( count, float3{ 1.f, 2.f, 3.f } );
    std::vector< float3 > b( count, float3{ 1.f, 2.f, 3.f } );
    std::vector< float3 > res( count );

    std::vector< All > all( count, All{ { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f } } );
    std::vector< Both > both( count, Both{ { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f } } );

    std::vector< int > index( count );
    int n = 0;
    std::generate( index.begin(), index.end(), [&]{ return n++; } );
    //std::random_shuffle( index.begin(), index.end() );

    PerformanceTimer timer;
    // uncomment version to test
    //sum( &a[0], &b[0], &res[0], &index[0], count );
    //sum( &both[0], &res[0], &index[0], count );
    //sum( &all[0], &index[0], count );
    std::cout << timer.time();
    return 0;
}

1
Bu önbellek yerellik hakkındaki şüphelerime çok yardımcı oluyor, teşekkürler!
Grimshaw

Güven verici bulduğum basit ama ilginç bir cevap :) Bu sonuçların farklı öğe sayıları (10.000.000 yerine 1000 mi?) -5 ayrı diziler ve değeri başka bir ayrı diziye depolama).
Awesomania

2

Kısa Cevap: Profil daha sonra optimize edin.

Uzun cevap:

Ancak, gerçek bir oyun uygulamasında bir sistemden onlarla bir şeyler yapmak için bileşen dizilerini yinelediğimde, neredeyse her zaman iki veya daha fazla bileşen türüyle aynı anda çalıştığımı fark ettim.

C ++ 'da iki farklı bitişik dizi yinelediğimde ve her döngüden her ikisinden veri kullandığımda bir sorun mu var?

Herhangi bir programlama dili için geçerli olduğu için C ++ önbellek hatalarından sorumlu değildir. Bu, modern CPU mimarisinin çalışma şekliyle ilgilidir.

Sorununuz, yetişkinlere yönelik optimizasyon olarak adlandırılabilecek duruma iyi bir örnek olabilir .

Bence, program bellek erişim kalıplarına bakmadan önbellek yeri için çok erken optimizasyon yaptınız. Ancak daha büyük soru, bu tür bir optimizasyona gerçekten ihtiyacınız var mı?

Agner's Fog, uygulamanızı profilini oluşturmadan ve / veya darboğazların nerede olduğunu bilmeden önce optimize etmemenizi önerir. (Tüm bunlar mükemmel kılavuzunda belirtilmiştir. Aşağıdaki bağlantı)

Sıralı erişime sahip olmayan büyük veri yapılarına sahip programlar oluşturuyorsanız ve önbellek çekişmesini önlemek istiyorsanız, önbelleğin nasıl düzenlendiğini bilmek yararlı olacaktır. Daha sezgisel kılavuzlardan memnunsanız bu bölümü atlayabilirsiniz.

Ne yazık ki yaptığınız şey aslında dizi başına bir bileşen türünün tahsis edilmesinin size daha iyi performans vereceğini varsayarken, gerçekte daha fazla önbellek özlemine ve hatta önbellek çekişmesine neden olmuş olabilirsiniz.

Kesinlikle onun mükemmel C ++ optimizasyon kılavuzuna bakmalısınız .

Sormak istediğim başka bir şey de, bileşenlerin hafızaya nasıl atıldığının doğası gereği, bileşenlere veya varlıklara referansları nasıl tutması gerektiğidir.

Şahsen ben en çok kullanılan bileşenleri tek bir bellek bloğunda bir araya getireceğim, böylece "yakın" adresleri olacak. Örneğin bir dizi şöyle görünecektir:

[{ID0 Transform Model PhysicsComp }{ID10 Transform Model PhysicsComp }{ID2 Transform Model PhysicsComp }..] ve performans "yeterince iyi" değilse oradan optimizasyona başlayın.


Benim sorum mimarimin performans üzerindeki etkileri hakkındaydı, mesele optimize etmek değil, işleri dahili olarak organize etmenin bir yolunu seçmekti . İçinde ne olursa olsun, daha sonra değiştirmek istemem durumunda oyun kodumun homojen bir şekilde etkileşmesini istiyorum. Verilerin nasıl saklanacağı konusunda ek öneriler sunsa bile yanıtınız iyiydi. Upvoted.
Grimshaw

Gördüğüm kadarıyla, bileşenlerin her biri varlık başına tek bir dizide birleştirilen ve tümü tek tek dizilerde türüne göre birleştirilen üç ana yol vardır ve doğru anladıysam, farklı Varlıkları bitişik olarak büyük bir dizide depolamanızı önerir, ve varlık başına tüm bileşenleri bir arada mıdır?
Grimshaw

@Grimshaw Cevabımda bahsettiğim gibi, mimarinizin normal tahsis modelinden daha iyi sonuç vereceği garanti edilmez. Uygulamalarınızın erişim düzenini gerçekten bilmediğiniz için. Bu optimizasyonlar genellikle bazı çalışmalardan / kanıtlardan sonra yapılır. Önerilerimle ilgili olarak, ilgili bileşenleri aynı bellekte ve farklı bileşenlerdeki diğer bileşenleri birlikte saklayın. Burası ya hep ya hiç arasında bir orta zemin. Yine de, kaç koşulun ortaya çıktığı göz önüne alındığında, mimarinizin sonucu nasıl etkileyeceğini tahmin etmenin zor olduğunu varsayıyorum.
concept3d

Downvoter açıklamak ister misiniz? Soruyu cevabımda göster. Daha iyi ama daha iyi bir cevap verin.
concept3d

1

Benim sorum şu, bu gibi durumlarda bir seferde doğrusal olarak bitişik bir diziyi yinelemediğim için, bileşenleri bu şekilde ayırmaktan elde edilen performans kazanımlarını hemen feda ediyor muyum?

Şansını söylemek gerekirse, "yatay" değişken boyutlu bir blokta bir varlığa bağlı bileşenleri bir araya getirmek yerine bileşen türü başına ayrı "dikey" dizilerle toplamda daha az önbellek özlemi elde etme şansınız vardır.

Bunun nedeni, ilk olarak "dikey" gösterimin daha az bellek kullanma eğiliminde olmasıdır. Bitişik olarak ayrılmış homojen diziler için hizalama konusunda endişelenmenize gerek yoktur. Bir bellek havuzuna ayrılmış homojen olmayan türlerle, dizideki ilk öğenin ikincisinden tamamen farklı boyut ve hizalama gereksinimleri olabileceğinden hizalama konusunda endişelenmeniz gerekir. Sonuç olarak, basit bir örnek gibi, genellikle dolgu eklemeniz gerekir:

// Assuming 8-bit chars and 64-bit doubles.
struct Foo
{
    // 1 byte
    char a;

    // 1 byte
    char b;
};

struct Bar
{
    // 8 bytes
    double opacity;

    // 8 bytes
    double radius;
};

Diyelim ki serpiştirmek Foove Barbunları yan yana bellekte saklamak istiyoruz :

// Assuming 8-bit chars and 64-bit doubles.
struct FooBar
{
    // 1 byte
    char a;

    // 1 byte
    char b;

    // 6 bytes padding for 64-bit alignment of 'opacity'

    // 8 bytes
    double opacity;

    // 8 bytes
    double radius;
};

Şimdi Foo ve Bar'ı ayrı bellek bölgelerinde saklamak için 18 bayt almak yerine, onları birleştirmek 24 bayt alır. Siparişi değiştirip değiştirmemeniz önemli değil:

// Assuming 8-bit chars and 64-bit doubles.
struct BarFoo
{
    // 8 bytes
    double opacity;

    // 8 bytes
    double radius;

    // 1 byte
    char a;

    // 1 byte
    char b;

    // 6 bytes padding for 64-bit alignment of 'opacity'
};

Erişim kalıplarını önemli ölçüde iyileştirmeden sıralı erişim bağlamında daha fazla bellek alırsanız, genellikle daha fazla önbellek özlemine maruz kalırsınız. Bunun ötesinde, bir varlıktan bir sonraki artışa ve değişken bir boyuta ulaşmak için adım atarak, hangi bileşenlere sahip olduğunuzu görmek için bir varlıktan diğerine geçmek için bellekte değişken boyutlu sıçramalar yapmanız gerekir. ' yeniden ilgileniyorum.

Dolayısıyla, bileşen türlerini depolarken yaptığınız gibi "dikey" bir temsili kullanmak aslında "yatay" alternatiflerden daha uygun olabilir. Bununla birlikte, dikey temsille önbellek özledim sorunu burada örneklenebilir:

resim açıklamasını buraya girin

Oklar basitçe varlığın bir bileşene "sahip olduğunu" gösterir. Her ikisine de sahip varlıkların tüm hareketlerine ve oluşturma bileşenlerine erişmeye çalışacak olursak, sonunda hafızadaki her yere atlıyoruz. Bu tür düzensiz erişim deseni, örneğin bir hareket bileşenine erişmek için verileri bir önbellek satırına yüklemenizi, ardından daha fazla bileşene erişmenizi ve eski verilerin çıkarılmasını sağlayabilir; bileşen. Bu, bileşenlerin bir listesine erişmek ve erişmek için aynı bellek bölgelerini bir kereden fazla bir önbellek hattına yüklemek çok israf olabilir.

Daha net görebilmek için bu karışıklığı biraz temizleyelim:

resim açıklamasını buraya girin

Bu tür bir senaryo ile karşılaşırsanız, genellikle oyunun başlamasından çok sonra, birçok bileşen ve varlık eklendikten ve kaldırıldıktan sonra olduğunu unutmayın. Genel olarak oyun başladığında, tüm varlıkları ve ilgili bileşenleri bir araya getirebilirsiniz, bu noktada iyi bir mekansal konuma sahip çok düzenli, sıralı bir erişim örüntüsüne sahip olabilirler. Çok sayıda kaldırma ve eklemeden sonra, yukarıdaki karışıklık gibi bir şey elde edebilirsiniz.

Bu durumu iyileştirmenin çok kolay bir yolu, bileşenlerinizi sahip oldukları varlık kimliğine / dizinine göre basitçe sıralamaktır. Bu noktada şöyle bir şey elde edersiniz:

resim açıklamasını buraya girin

Ve bu çok daha önbellek dostu bir erişim modeli. Mükemmel değil, çünkü burada ve orada bazı oluşturma ve hareket bileşenlerini atlamamız gerektiğini görebiliyoruz, çünkü sistemimiz sadece ikisine birden sahip olan varlıklarla ilgileniyor ve bazı varlıkların yalnızca bir hareket bileşeni ve bazılarının yalnızca bir oluşturma bileşeni var , ancak en azından bazı bitişik bileşenleri işleyebileceksiniz (uygulamada daha çok, tipik olarak, genellikle ilgili ilgili bileşenleri ekleyeceksiniz, örneğin sisteminizde belki de bir hareket bileşenine sahip daha fazla varlık bir oluşturma bileşenine sahip olacak değil).

En önemlisi, bunları sıraladıktan sonra, verileri bir bellek bölgesini önbellek satırına yüklemezsiniz, ancak daha sonra tek bir döngüde yeniden yüklersiniz.

Ve bu son derece karmaşık bir tasarım gerektirmez, sadece arada bir doğrusal zaman yarıçapı sıralama geçişi, belki de belirli bir bileşen türü için bir grup bileşeni ekleyip çıkardıktan sonra, bu noktada sıralanması gerekiyor. Makul şekilde uygulanan bir radyus sıralaması (hatta paralelleştirebilirsiniz, ki ben bunu paralelleştirebilirsiniz), burada gösterildiği gibi dört çekirdekli i7'de yaklaşık 6 ms'de bir milyon öğe sıralayabilir:

Sorting 1000000 elements 32 times...
mt_sort_int: {0.203000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_sort: {1.248000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_radix_sort: {0.202000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
std::sort: {1.810000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
qsort: {2.777000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]

Yukarıdaki, bir milyon unsuru 32 kez memcpysıralamaktır (sıralamadan önce ve sonra sonuçlara kadar geçen süre dahil ). Çoğu zaman aslında sıralamak için bir milyondan fazla bileşene sahip olmayacağınızı varsayıyorum, bu yüzden şimdi ve orada dikkat çekici bir kare hızı kekemesine neden olmadan bunu kolayca gizleyebilmelisiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.