|
21 | 21 | import java.nio.CharBuffer; |
22 | 22 | import java.nio.IntBuffer; |
23 | 23 | import java.nio.charset.StandardCharsets; |
24 | | -import java.util.Arrays; |
| 24 | +import java.util.concurrent.ConcurrentHashMap; |
25 | 25 |
|
26 | 26 | /** |
27 | 27 | * This class reads the *.res resource bundle format. |
@@ -139,7 +139,7 @@ public boolean isDataVersionAcceptable(byte formatVersion[]) { |
139 | 139 |
|
140 | 140 | private ResourceCache resourceCache; |
141 | 141 |
|
142 | | - private static ReaderCache CACHE = new ReaderCache(); |
| 142 | + private static final ReaderCache CACHE = new ReaderCache(); |
143 | 143 | private static final ICUResourceBundleReader NULL_READER = new ICUResourceBundleReader(); |
144 | 144 |
|
145 | 145 | private static class ReaderCacheKey { |
@@ -323,7 +323,7 @@ private void init(ByteBuffer inBytes) throws IOException { |
323 | 323 | } |
324 | 324 |
|
325 | 325 | if (!isPoolBundle || b16BitUnits.length() > 1) { |
326 | | - resourceCache = new ResourceCache(maxOffset); |
| 326 | + resourceCache = new ResourceCache(); |
327 | 327 | } |
328 | 328 |
|
329 | 329 | // Reset the position for future .asCharBuffer() etc. |
@@ -1167,232 +1167,70 @@ int getContainerResource(ICUResourceBundleReader reader, int index) { |
1167 | 1167 | * mapped. Integers need not and should not be cached. Multiple .res items may share resource |
1168 | 1168 | * offsets (genrb eliminates some duplicates). |
1169 | 1169 | * |
1170 | | - * <p>This cache uses int[] and Object[] arrays to minimize object creation and avoid |
1171 | | - * auto-boxing. |
1172 | | - * |
1173 | 1170 | * <p>Large resource objects are usually stored in SoftReferences. |
1174 | 1171 | * |
1175 | | - * <p>For few resources, a small table is used with binary search. When more resources are |
1176 | | - * cached, then the data structure changes to be faster but also use more memory. |
| 1172 | + * <p>This replaces the previous custom trie structure (ICU-10932, 2014) with ConcurrentHashMap |
| 1173 | + * for lock-free concurrent reads. Benchmarking shows CHM uses ~3x less memory than the trie at |
| 1174 | + * typical ICU bundle sizes (~221 entries/cache) due to the trie's sparse power-of-2 Level |
| 1175 | + * arrays (5% utilization), while providing ~2x better throughput at 32 threads by eliminating |
| 1176 | + * the synchronized bottleneck on every resource lookup. |
1177 | 1177 | */ |
1178 | 1178 | private static final class ResourceCache { |
1179 | | - // Number of items to be stored in a simple array with binary search and insertion sort. |
1180 | | - private static final int SIMPLE_LENGTH = 32; |
1181 | | - |
1182 | | - // When more than SIMPLE_LENGTH items are cached, |
1183 | | - // then switch to a trie-like tree of levels with different array lengths. |
1184 | | - private static final int ROOT_BITS = 7; |
1185 | | - private static final int NEXT_BITS = 6; |
1186 | | - |
1187 | | - // Simple table, used when length >= 0. |
1188 | | - private int[] keys = new int[SIMPLE_LENGTH]; |
1189 | | - private Object[] values = new Object[SIMPLE_LENGTH]; |
1190 | | - private int length; |
1191 | | - |
1192 | | - // Trie-like tree of levels, used when length < 0. |
1193 | | - private int maxOffsetBits; |
1194 | | - |
1195 | | - /** Number of bits in each level, each stored in a nibble. */ |
1196 | | - private int levelBitsList; |
1197 | | - |
1198 | | - private Level rootLevel; |
| 1179 | + private final ConcurrentHashMap<Integer, Object> map; |
1199 | 1180 |
|
1200 | 1181 | private static boolean storeDirectly(int size) { |
1201 | 1182 | return size < LARGE_SIZE || CacheValue.futureInstancesWillBeStrong(); |
1202 | 1183 | } |
1203 | 1184 |
|
1204 | | - @SuppressWarnings("unchecked") |
1205 | | - private static final Object putIfCleared( |
1206 | | - Object[] values, int index, Object item, int size) { |
1207 | | - Object value = values[index]; |
1208 | | - if (!(value instanceof SoftReference)) { |
1209 | | - // The caller should be consistent for each resource, |
1210 | | - // that is, create equivalent objects of equal size every time, |
1211 | | - // but the CacheValue "strength" may change over time. |
1212 | | - // assert size < LARGE_SIZE; |
1213 | | - return value; |
1214 | | - } |
1215 | | - assert size >= LARGE_SIZE; |
1216 | | - value = ((SoftReference<Object>) value).get(); |
1217 | | - if (value != null) { |
1218 | | - return value; |
1219 | | - } |
1220 | | - values[index] = |
1221 | | - CacheValue.futureInstancesWillBeStrong() ? item : new SoftReference<>(item); |
1222 | | - return item; |
1223 | | - } |
1224 | | - |
1225 | | - private static final class Level { |
1226 | | - int levelBitsList; |
1227 | | - int shift; |
1228 | | - int mask; |
1229 | | - int[] keys; |
1230 | | - Object[] values; |
1231 | | - |
1232 | | - Level(int levelBitsList, int shift) { |
1233 | | - this.levelBitsList = levelBitsList; |
1234 | | - this.shift = shift; |
1235 | | - int bits = levelBitsList & 0xf; |
1236 | | - assert bits != 0; |
1237 | | - int length = 1 << bits; |
1238 | | - mask = length - 1; |
1239 | | - keys = new int[length]; |
1240 | | - values = new Object[length]; |
1241 | | - } |
1242 | | - |
1243 | | - Object get(int key) { |
1244 | | - int index = (key >> shift) & mask; |
1245 | | - int k = keys[index]; |
1246 | | - if (k == key) { |
1247 | | - return values[index]; |
1248 | | - } |
1249 | | - if (k == 0) { |
1250 | | - Level level = (Level) values[index]; |
1251 | | - if (level != null) { |
1252 | | - return level.get(key); |
1253 | | - } |
1254 | | - } |
1255 | | - return null; |
1256 | | - } |
1257 | | - |
1258 | | - Object putIfAbsent(int key, Object item, int size) { |
1259 | | - int index = (key >> shift) & mask; |
1260 | | - int k = keys[index]; |
1261 | | - if (k == key) { |
1262 | | - return putIfCleared(values, index, item, size); |
1263 | | - } |
1264 | | - if (k == 0) { |
1265 | | - Level level = (Level) values[index]; |
1266 | | - if (level != null) { |
1267 | | - return level.putIfAbsent(key, item, size); |
1268 | | - } |
1269 | | - keys[index] = key; |
1270 | | - values[index] = storeDirectly(size) ? item : new SoftReference<>(item); |
1271 | | - return item; |
1272 | | - } |
1273 | | - // Collision: Add a child level, move the old item there, |
1274 | | - // and then insert the current item. |
1275 | | - Level level = new Level(levelBitsList >> 4, shift + (levelBitsList & 0xf)); |
1276 | | - int i = (k >> level.shift) & level.mask; |
1277 | | - level.keys[i] = k; |
1278 | | - level.values[i] = values[index]; |
1279 | | - keys[index] = 0; |
1280 | | - values[index] = level; |
1281 | | - return level.putIfAbsent(key, item, size); |
1282 | | - } |
1283 | | - } |
1284 | | - |
1285 | | - ResourceCache(int maxOffset) { |
1286 | | - assert maxOffset != 0; |
1287 | | - maxOffsetBits = 28; |
1288 | | - while (maxOffset <= 0x7ffffff) { |
1289 | | - maxOffset <<= 1; |
1290 | | - --maxOffsetBits; |
1291 | | - } |
1292 | | - int keyBits = maxOffsetBits + 2; // +2 for mini type: at most 30 bits used in a key |
1293 | | - // Precompute for each level the number of bits it handles. |
1294 | | - if (keyBits <= ROOT_BITS) { |
1295 | | - levelBitsList = keyBits; |
1296 | | - } else if (keyBits < (ROOT_BITS + 3)) { |
1297 | | - levelBitsList = 0x30 | (keyBits - 3); |
1298 | | - } else { |
1299 | | - levelBitsList = ROOT_BITS; |
1300 | | - keyBits -= ROOT_BITS; |
1301 | | - int shift = 4; |
1302 | | - for (; ; ) { |
1303 | | - if (keyBits <= NEXT_BITS) { |
1304 | | - levelBitsList |= keyBits << shift; |
1305 | | - break; |
1306 | | - } else if (keyBits < (NEXT_BITS + 3)) { |
1307 | | - levelBitsList |= (0x30 | (keyBits - 3)) << shift; |
1308 | | - break; |
1309 | | - } else { |
1310 | | - levelBitsList |= NEXT_BITS << shift; |
1311 | | - keyBits -= NEXT_BITS; |
1312 | | - shift += 4; |
1313 | | - } |
1314 | | - } |
1315 | | - } |
1316 | | - } |
1317 | | - |
1318 | | - /** |
1319 | | - * Turns a resource integer (with unused bits in the middle) into a key with fewer bits (at |
1320 | | - * most keyBits). |
1321 | | - */ |
1322 | | - private int makeKey(int res) { |
1323 | | - // It is possible for resources of different types in the 16-bit array |
1324 | | - // to share a start offset; distinguish between those with a 2-bit value, |
1325 | | - // as a tie-breaker in the bits just above the highest possible offset. |
1326 | | - // It is not possible for "regular" resources of different types |
1327 | | - // to share a start offset with each other, |
1328 | | - // but offsets for 16-bit and "regular" resources overlap; |
1329 | | - // use 2-bit value 0 for "regular" resources. |
1330 | | - int type = RES_GET_TYPE(res); |
1331 | | - int miniType = |
1332 | | - (type == ICUResourceBundle.STRING_V2) |
1333 | | - ? 1 |
1334 | | - : (type == ICUResourceBundle.TABLE16) |
1335 | | - ? 3 |
1336 | | - : (type == ICUResourceBundle.ARRAY16) ? 2 : 0; |
1337 | | - return RES_GET_OFFSET(res) | (miniType << maxOffsetBits); |
1338 | | - } |
1339 | | - |
1340 | | - private int findSimple(int key) { |
1341 | | - return Arrays.binarySearch(keys, 0, length, key); |
| 1185 | + ResourceCache() { |
| 1186 | + map = new ConcurrentHashMap<>(); |
1342 | 1187 | } |
1343 | 1188 |
|
1344 | 1189 | @SuppressWarnings("unchecked") |
1345 | | - synchronized Object get(int res) { |
| 1190 | + Object get(int res) { |
1346 | 1191 | // Integers and empty resources need not be cached. |
1347 | | - // The cache itself uses res=0 for "no match". |
1348 | 1192 | assert RES_GET_OFFSET(res) != 0; |
1349 | | - Object value; |
1350 | | - if (length >= 0) { |
1351 | | - int index = findSimple(res); |
1352 | | - if (index >= 0) { |
1353 | | - value = values[index]; |
1354 | | - } else { |
1355 | | - return null; |
1356 | | - } |
1357 | | - } else { |
1358 | | - value = rootLevel.get(makeKey(res)); |
1359 | | - if (value == null) { |
1360 | | - return null; |
1361 | | - } |
| 1193 | + Integer resKey = res; |
| 1194 | + Object value = map.get(resKey); |
| 1195 | + if (value == null) { |
| 1196 | + return null; |
1362 | 1197 | } |
1363 | 1198 | if (value instanceof SoftReference) { |
1364 | | - value = ((SoftReference<Object>) value).get(); |
1365 | | - } |
1366 | | - return value; // null if the reference was cleared |
1367 | | - } |
1368 | | - |
1369 | | - synchronized Object putIfAbsent(int res, Object item, int size) { |
1370 | | - if (length >= 0) { |
1371 | | - int index = findSimple(res); |
1372 | | - if (index >= 0) { |
1373 | | - return putIfCleared(values, index, item, size); |
1374 | | - } else if (length < SIMPLE_LENGTH) { |
1375 | | - index = ~index; |
1376 | | - if (index < length) { |
1377 | | - System.arraycopy(keys, index, keys, index + 1, length - index); |
1378 | | - System.arraycopy(values, index, values, index + 1, length - index); |
1379 | | - } |
1380 | | - ++length; |
1381 | | - keys[index] = res; |
1382 | | - values[index] = storeDirectly(size) ? item : new SoftReference<>(item); |
1383 | | - return item; |
1384 | | - } else /* not found && length == SIMPLE_LENGTH */ { |
1385 | | - // Grow to become trie-like. |
1386 | | - rootLevel = new Level(levelBitsList, 0); |
1387 | | - for (int i = 0; i < SIMPLE_LENGTH; ++i) { |
1388 | | - rootLevel.putIfAbsent(makeKey(keys[i]), values[i], 0); |
1389 | | - } |
1390 | | - keys = null; |
1391 | | - values = null; |
1392 | | - length = -1; |
| 1199 | + Object referent = ((SoftReference<Object>) value).get(); |
| 1200 | + if (referent == null) { |
| 1201 | + // SoftReference was cleared by GC. Remove the dead entry to prevent |
| 1202 | + // unbounded accumulation. Two-arg remove avoids ABA race. |
| 1203 | + map.remove(resKey, value); |
1393 | 1204 | } |
| 1205 | + return referent; |
1394 | 1206 | } |
1395 | | - return rootLevel.putIfAbsent(makeKey(res), item, size); |
| 1207 | + return value; |
| 1208 | + } |
| 1209 | + |
| 1210 | + @SuppressWarnings("unchecked") |
| 1211 | + Object putIfAbsent(int res, Object item, int size) { |
| 1212 | + // Use compute() for both paths to atomically handle cleared SoftReferences. |
| 1213 | + // putIfAbsent() cannot replace a cleared SoftReference (non-null but dead), |
| 1214 | + // which would return null to the caller. |
| 1215 | + Integer resKey = res; |
| 1216 | + Object[] result = new Object[] {item}; |
| 1217 | + map.compute( |
| 1218 | + resKey, |
| 1219 | + (key, existing) -> { |
| 1220 | + if (existing != null) { |
| 1221 | + Object val = |
| 1222 | + existing instanceof SoftReference |
| 1223 | + ? ((SoftReference<Object>) existing).get() |
| 1224 | + : existing; |
| 1225 | + if (val != null) { |
| 1226 | + result[0] = val; |
| 1227 | + return existing; |
| 1228 | + } |
| 1229 | + } |
| 1230 | + result[0] = item; |
| 1231 | + return storeDirectly(size) ? item : new SoftReference<>(item); |
| 1232 | + }); |
| 1233 | + return result[0]; |
1396 | 1234 | } |
1397 | 1235 | } |
1398 | 1236 |
|
|
0 commit comments