回答編集履歴

3

コードの間違いを訂正

2017/10/10 07:59

投稿

KSwordOfHaste
KSwordOfHaste

スコア18404

answer CHANGED
@@ -5,7 +5,7 @@
5
5
  import java.util.*
6
6
 
7
7
  class LruCache3<K, V>(private val capacity: Int,
8
- private val initializer: (K) -> V) : Cache<K, V> {
8
+ private val initializer: (K) -> V) {
9
9
  val map = object: LinkedHashMap<K, V>(capacity, 1.0F, true) {
10
10
  override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
11
11
  return size > capacity
@@ -13,7 +13,7 @@
13
13
  }
14
14
  val lock = java.util.concurrent.locks.StampedLock()
15
15
 
16
- override operator fun get(key: K): V {
16
+ operator fun get(key: K): V {
17
17
  val stamp = lock.writeLock()
18
18
  try {
19
19
  return map.getOrPut(key) { initializer(key) }
@@ -47,7 +47,7 @@
47
47
  ということでgetは単に次のようにすればよいと思います。
48
48
 
49
49
  リスト1
50
- `override operator fun get(key: K): V = map.getOrPut(key) { initializer(key) }`
50
+ `operator fun get(key: K): V = map.getOrPut(key) { initializer(key) }`
51
51
 
52
52
  ---
53
53
  ※:計算関数が副作用を持つ場合の問題
@@ -85,6 +85,6 @@
85
85
 
86
86
  ということで、getOrPutをjava.util.concurrent.locks.ReentrantLockなどにより自前で同期するか、あるいはgetOrPutではなくjava側で同期が保証されるcomputeIfAbsentを使うなどの対処が必要と思います。後者の方が以下のようにシンプルに書けます。
87
87
 
88
- `override operator fun get(key: K): V = map.computeIfAbsent(key, initializer)`
88
+ `operator fun get(key: K): V = map.computeIfAbsent(key, initializer)`
89
89
 
90
90
  自分がKotlinに暗いのとimport文が質問文に明記されていなかったため、本指摘があたりかどうか確信がもてませんが、一応コメントいたします。

2

高速化について追記

2017/10/10 07:59

投稿

KSwordOfHaste
KSwordOfHaste

スコア18404

answer CHANGED
@@ -1,3 +1,43 @@
1
+ ###若干高速かも知れないコード(追記3)
2
+ 前回まで指摘ばかりのコメントでしたが、高速化についてコメントしてみます。
3
+
4
+ ```kotlin
5
+ import java.util.*
6
+
7
+ class LruCache3<K, V>(private val capacity: Int,
8
+ private val initializer: (K) -> V) : Cache<K, V> {
9
+ val map = object: LinkedHashMap<K, V>(capacity, 1.0F, true) {
10
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
11
+ return size > capacity
12
+ }
13
+ }
14
+ val lock = java.util.concurrent.locks.StampedLock()
15
+
16
+ override operator fun get(key: K): V {
17
+ val stamp = lock.writeLock()
18
+ try {
19
+ return map.getOrPut(key) { initializer(key) }
20
+ } finally {
21
+ lock.unlockWrite(stamp);
22
+ }
23
+ }
24
+
25
+ // toStringも上記と同様にwriteLockの中で実行。記載は省略します
26
+ }
27
+ ```
28
+ 上記は機能的に質問者さんの実装と同等ですが、同期にjavaのsynchronizedブロックではなく、StampedLock(compare and write命令を使うもの)で実装したものです。キャッシュサイズは少し増やして100、アクセスはほぼヒットとなるよう0~99の範囲とし、javaの並列ストリームパイプライン(以下)で実行し、20回の平均を見てみました。また比較のため単一スレッドでもやってみました。(なお自分のPCは2コアしかありません...)
29
+ `IntStream.range(0, 1000000).parallel().forEach { cache[it / 15] }`
30
+
31
+ |対象クラス|シリアル/パラレル|標本平均(ms)|標本標準偏差(ms)|
32
+ |:--|:--|--:|--:|
33
+ |オリジナル|シリアル|63|16|
34
+ |オリジナル|パラレル|166|17|
35
+ |StampedLock版|シリアル|62|11|
36
+ |StampedLock版|パラレル|82|16|
37
+
38
+ 単一スレッドの場合、競合がおきないため並列実行よりずっと高速に動作しますが、どちらの実装も差はでません。一方、並列処理ではsynchronizedブロックよりcompare & writeの方が早いという結果が伺えます。
39
+
40
+ ###元のコメント
1
41
  解答ではありませんが。
2
42
 
3
43
  質問者さんのキャッシュクラスの実装に問題があると思います。

1

追記

2017/10/10 06:32

投稿

KSwordOfHaste
KSwordOfHaste

スコア18404

answer CHANGED
@@ -36,4 +36,15 @@
36
36
  Caused by: kotlin.KotlinNullPointerException
37
37
  at test.LruCache.get(SyncMap.kt:24)
38
38
  ...
39
- ```
39
+ ```
40
+
41
+ ---
42
+ 追記2:すみません、上記の指摘には足りない点があった気がします
43
+
44
+ Collections.synchronizedMapメソッドはjava.util.Collectionsのメソッドだと思います。そうだとするとkotlinはmapが単なるMutableMapであると解釈しアトミックでないgetOrPut拡張関数が用いられます。これがjava.util.concurrent.ConcurrentMapであればアトミック版のgetOrPutが用いられるのですが、ConcurrentMapにはCollections.synchronizedMapのようにコレクションをラップしてくれるような適当なクラスが用意されておらず、LinkedHashMapの特徴を生かすことができません。
45
+
46
+ ということで、getOrPutをjava.util.concurrent.locks.ReentrantLockなどにより自前で同期するか、あるいはgetOrPutではなくjava側で同期が保証されるcomputeIfAbsentを使うなどの対処が必要と思います。後者の方が以下のようにシンプルに書けます。
47
+
48
+ `override operator fun get(key: K): V = map.computeIfAbsent(key, initializer)`
49
+
50
+ 自分がKotlinに暗いのとimport文が質問文に明記されていなかったため、本指摘があたりかどうか確信がもてませんが、一応コメントいたします。