Bileşik anahtarlar, haritanız veya önbellek aramanız için "anahtarı" tanımlamak üzere bir veri kombinasyonunun gerekli olduğu durumlardır. Bunun bir örneği, bir kullanıcının rolünün yanı sıra müşteri adına dayalı olarak değerleri önbelleğe almanız gereken yer olabilir. Böyle bir durumda, önbelleğinizin bu iki (veya daha fazla) kriterin her birine dayalı benzersiz değerleri depolayabilmesi gerekir.
Bileşik anahtarların kodda işlenmesinin birkaç farklı yolu vardır.
En çok atlanan ilk cevap, kriterleri anahtar olarak kullanılacak bir dizede birleştirmektir. Çok basit ve fazla çaba gerektirmez:
private String getMapKey(Long userId, String userLocale) { return userId + "." userLocale; }
Bu, sorunu çözmenin oldukça basit bir yoludur. Önbellek anahtarı insan tarafından okunabilir bir biçimde olduğundan, dize anahtarının kullanılması hata ayıklamayı ve araştırmaları kolaylaştırabilir. Ancak bu yaklaşımla ilgili bilinmesi gereken birkaç sorun vardır:
Haritayla her etkileşimde yeni bir dizenin oluşturulmasını gerektirir. Bu dize tahsisi genellikle küçük olsa da, haritaya sık sık erişilirse, zaman alan ve çöp toplanması gereken çok sayıda tahsise yol açabilir. Dize tahsisinin boyutu, anahtarınızın bileşenlerinin büyüklüğüne veya kaç taneye sahip olduğunuza bağlı olarak daha büyük olabilir.
Oluşturduğunuz bileşik anahtarın başka bir anahtar değerine dönüştürülemeyeceğinden emin olmanız gerekir:
public String getMapKey(Integer groupId, Integer accessType) { return groupId.toString() + accessType.toString(); }
Yukarıda, groupId = 1 ve AccessType = 23'e sahip olsaydınız, bu, groupId = 12 ve AccessType = 3 ile aynı önbellek anahtarı olurdu. Dizelerin arasına bir ayırıcı karakter ekleyerek bu tür çakışmaları önleyebilirsiniz. Ancak anahtarın isteğe bağlı kısımlarına dikkat edin:
public String getMapKey(String userProvidedString, String extensionName) { return userProvidedString + (extensionName == null ? "" : ("." + extensionName)); }
Yukarıdaki örnekte extensionName, anahtarın isteğe bağlı bir parçasıdır. extensionName isteğe bağlıysa userProvidedString bir ayırıcı ve geçerli extensionName içerebilir ve erişmemesi gereken önbellek verilerine erişim sağlayabilir.
Dizeleri kullanırken, tuşlarda herhangi bir çarpışmayı önlemek için verilerinizi nasıl birleştirdiğinizi düşünmek isteyeceksiniz. Özellikle anahtar için kullanıcı tarafından oluşturulan herhangi bir giriş çevresinde.
Diğer bir seçenek de anahtarları hiçbir şekilde birleştirmemek ve bunun yerine veri yapılarınızı iç içe yerleştirmektir (Haritaların Haritaları):
Map<Integer, Map<String, String>> groupAndLocaleMap = new HashMap<>(); groupAndLocaleMap.computeIfAbsent(userId, k -> new HashMap()).put(userLocale, mapValue);
Bunun avantajı, anahtarlar için aktarılan değerler zaten tahsis edildiğinden, haritalarla etkileşimde bulunulurken herhangi bir yeni hafıza tahsis edilmesine gerek olmamasıdır. Nihai değere ulaşmak için birden fazla arama yapmanız gerekse de haritalar daha küçük olacaktır.
Ancak bu yaklaşımın dezavantajı, yuvalama derinleştikçe daha karmaşık hale gelmesidir. Yalnızca iki düzeyde bile haritanın başlatılması kafa karıştırıcı görünebilir. 3 veya daha fazla veri parçasıyla uğraşmaya başladığınızda bu, kodunuzun çok ayrıntılı olmasına yol açabilir. Bunun da ötesinde, her düzey boş işaretçilerden kaçınmak için boş denetim gerektirir.
Bazı "anahtar parçalar" da harita anahtarı olarak iyi çalışmayabilir. Diziler veya koleksiyonlar, içeriklerini karşılaştıran varsayılan eşittir yöntemlerine sahip değildir. Yani ya bunları uygulamanız ya da başka bir alternatif kullanmanız gerekir.
İç içe haritaların kullanılması, anahtarlarınızın her seviyesinin ne kadar benzersiz olduğuna bağlı olarak alan açısından daha az verimli olabilir.
Son seçenek, anahtar değerlerini bir dizede birleştirmek yerine anahtar için özel bir nesne oluşturmaktır:
private class MapKey { private final int userId; private final String userLocale; public MapKey(int userId, String userLocale) { this.userId = userId; this.userLocale = userLocale; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MapKey mapKey = (MapKey) o; return userId == mapKey.userId && Objects.equals(userLocale, mapKey.userLocale); } @Override public int hashCode() { return Objects.hash(userId, userLocale); } }
Her etkileşim hala yeni bir nesne için yeni bir bellek tahsisi gerektiriyor. Nesne anahtarı tahsisi, bileşik dize için gerekenden önemli ölçüde daha küçüktür. Bunun nedeni, anahtarı oluşturan parçaların dize olarak yeniden tahsis edilmesine gerek olmamasıdır. Bunun yerine yalnızca sarma nesnesi anahtarı yeni bellek gerektirir.
Bileşik bir anahtar nesnesi aynı zamanda anahtar eşitliği ve hashcode uygulamalarında özelleştirmelere de izin verebilir. Bir dizedeki büyük harf kullanımını göz ardı etmek veya bir diziyi veya koleksiyonu bir anahtarın parçası olarak kullanmak gibi.
Buradaki dezavantaj ise yine bileşik dizgeden çok daha fazla kod gerektirmesidir. Ve haritanız için anahtar sınıfta geçerli eşit ve karma kod sözleşmelerine sahip olduğunuzdan emin olmanızı gerektirir.
Peki hangisini seçmeliyim?
Genel olarak konuşursak, bileşik bir dize anahtarı kullanmanızı öneririm. Basit ve anlaşılması kolaydır, en az kod gerektirir ve daha sonra hata ayıklaması en kolaydır. Muhtemelen en yavaş performansa sahip olsa da, basit, okunabilir kod yazmak genellikle diğer iki seçenekten birini kullanarak elde edeceğiniz faydalardan daha önemlidir. Hatırlamak:
“Erken optimizasyon tüm kötülüklerin köküdür” Donald Knuth
Harita/önbellek aramanızın performans darboğazı olacağına dair kanıtınız veya nedeniniz yoksa okunabilirliği tercih edin.
Ancak haritanızın veya önbelleğinizin veriminin çok yüksek olduğu bir senaryodaysanız diğer iki seçenekten birine geçmek iyi olabilir. Her üçünün de performans açısından ve bellek ayırma boyutları açısından birbirleriyle nasıl karşılaştırıldığını görelim.
Yukarıdaki 3 senaryoyu test etmek için, bir bileşik anahtar için 3 senaryonun tamamının aynı uygulamasını kopyalayacak bir kod yazdım. Anahtarın kendisi bir tam sayı değeri, bir dize değeri ve bir uzun değerden oluşur. Her üç uygulama da anahtarları oluşturmak için her çalıştırmada aynı test verilerini kullandı.
Tüm çalıştırmalar haritada 1 milyon kayıtla gerçekleştirildi (Java'nın hashmap'i kullanıldı). Anahtarın farklı boyut kombinasyonlarıyla oluşturulması için 3 çalışma yapıldı:
100 inç, 100 dize, 100 uzun — 1 milyon benzersiz anahtar
1 int, 1 string, 1.000.000 long — 1 milyon benzersiz anahtar
1.000.000 inç, 1 dize, 1 uzun — 1 milyon benzersiz anahtar
Öncelikle her haritanın yığında ne kadar yer kapladığına bakalım. Bu önemlidir çünkü uygulamanızı çalıştırmak için ne kadar belleğe ihtiyaç duyulacağını etkiler.
Burada belirtilmesi gereken ilginç ve bariz bir not var: Son senaryoda (1.000.000 inç), iç içe haritaların boyutu diğerlerinden önemli ölçüde daha büyük. Bunun nedeni, bu senaryoda iç içe geçmiş haritaların 1 milyon girişli 1 birinci düzey harita oluşturmasıdır. Daha sonra ikinci ve üçüncü seviyeler için tek girişle 1 milyon harita oluşturur.
Tüm bu iç içe geçmiş haritalar fazladan yük depolar ve çoğunlukla boştur. Bu elbette uç bir örnek ama bir noktaya değinmek için bunu göstermek istedim. Yuva haritaları uygulamasını kullanırken benzersizlik (ve bu benzersizliğin sıralaması) çok önemlidir.
Sıralamayı 1, 1, 1 milyona çevirirseniz aslında en düşük depolama gereksinimini elde edersiniz.
Diğer iki senaryoda, iç içe eşleme en verimli olanıdır; özel anahtar nesnesi ikinci sırada gelir ve dize tuşları en sonda gelir.
Şimdi bu haritaların her birini sıfırdan oluşturmak için gereken süreye bakalım:
Yine, iç içe haritaların bellek tahsisi açısından 1 milyon 1–1 senaryosunda en kötü performansı gösterdiğini görüyoruz, ancak o zaman bile CPU süresi açısından diğerlerinden daha iyi performans gösteriyor. Yukarıda, özel bir anahtar nesnesinin kullanılması biraz daha yavaştır ve iç içe geçmiş anahtarlardan daha fazla bellek tahsisi gerektirirken, String anahtarının her durumda en kötü performansı nasıl gösterdiğini de görebiliriz.
Son olarak en yüksek verim senaryosuna ve okumanın ne kadar etkili olduğuna bakalım. 1 milyon okuma işlemi gerçekleştirdik (oluşturulan her anahtar için 1); var olmayan anahtarları dahil etmedik.
Dize tabanlı anahtar aramanın ne kadar yavaş olduğunu gerçekten burada görüyoruz. Bu, açık ara en yavaş olanıdır ve 3 seçenek arasında açık ara en fazla belleği ayırır. Özel anahtar nesnesi, iç içe haritalar uygulamasına "yakın" bir performans sergiler ancak yine de küçük bir farkla sürekli olarak daha yavaştır.
Ancak, arama belleği tahsislerinde iç içe haritaların ne kadar iyi parladığına dikkat edin. Hayır, bu grafikte bir aksaklık değil; iç içe haritalarda bir değer aramak, aramayı gerçekleştirmek için ekstra bellek ayırmayı gerektirmez. Bu nasıl mümkün olabilir?
Bileşik nesneleri bir dize anahtarında birleştirirken, her seferinde yeni bir dize nesnesi için bellek ayırmanız gerekir:
private String lookup(int key1, String key2, long key3) { return map.get(key1 + "." + key2 + "." + key3); }
Bileşik anahtar kullanırken yine de yeni bir anahtar nesnesi için bellek ayırmanız gerekir. Ancak bu nesnenin üyeleri zaten oluşturulmuş ve referans verilmiş olduğundan, yine de yeni bir dizeden çok daha azını ayırır:
private String lookup(int key1, String key2, long key3) { return map.get(new MapKey(key1, key2, key3)); }
Ancak iç içe haritalar uygulaması, arama sırasında yeni bellek tahsisi gerektirmez. Verilen parçaları iç içe geçmiş haritaların her birinin anahtarı olarak yeniden kullanıyorsunuz:
private String lookup(int key1, String key2, long key3) { return map.get(key1).get(key2).get(key3); }
Peki yukarıdakilere göre en performanslı hangisi?
Neredeyse tüm senaryolarda iç içe haritaların en üstte çıktığını görmek kolaydır. Çoğu kullanım durumunda ham performans arıyorsanız, bu muhtemelen en iyi seçenektir. Ancak kullanım durumlarınızı doğrulamak için kendi testlerinizi yapmalısınız.
Anahtar nesne, iç içe haritaların uygulamanız için kullanılması pratik olmadığında veya imkansız hale geldiğinde çok iyi bir genel amaçlı seçenek sağlar. Bileşik dize anahtarı, uygulanması en kolay olmasına rağmen neredeyse her zaman en yavaş olan olacaktır.
Bileşik anahtarları uygulamaya çalışırken göz önünde bulundurmanız gereken son nokta, yukarıdakileri birleştirebilmenizdir. Örneğin, ilk veya ikinci seviye için iç içe haritalar kullanabilir ve daha sonra daha derin seviyeleri basitleştirmek için bileşik anahtar nesnesi kullanabilirsiniz.
Bu, depolama ve arama performansını optimize ederken verilerinizin hızlı aramalar için bölümlenmiş halde kalmasını sağlayabilir. Ayrıca kodunuzu da okunabilir tutun.
Çoğu zaman basit tutun. En kolay seçenek buysa ve performans önemli bir sorun değilse, bileşik anahtarlarınızı bir haritada veya önbellekte depolamak için bir dize anahtarında birleştirin.
Performansın kritik olduğu senaryolarda kendi testinizi yaptığınızdan emin olun. Ancak çoğu durumda iç içe haritaların kullanılması en performanslı yöntem olacaktır. Ayrıca muhtemelen en küçük depolama gereksinimlerine sahip olacaktır. Ve bileşik anahtarlar, iç içe yerleştirme eşlemeleri kullanışsız hale geldiğinde hala performanslı bir alternatiftir.
Burada da yayınlandı