HashMap.clear ()에서 Arrays.fill ()이 더 이상 사용되지 않는 이유는 무엇입니까?
구현에서 이상한 것을 발견했습니다 HashMap.clear()
. 이것이 OpenJDK 7u40 에서 어떻게 보 였는지입니다 .
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
그리고 이것은 OpenJDK 8u40의 모습입니다 .
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
이제 table
지도가 비어 있으면이 null 일 수 있으므로 지역 변수에 대한 추가 검사 및 캐싱이 필요하다는 것을 이해합니다. 그러나 왜 Arrays.fill()
for-loop 로 대체 되었습니까?
이 커밋 에서 변경 사항이 도입 된 것 같습니다 . 불행히도 일반 for 루프가 Arrays.fill()
. 더 빠릅니까? 아니면 더 안전한가요?
나는 의견에서 제안 된 세 가지 더 합리적인 버전을 요약하려고 노력할 것입니다.
@Holger 말한다 :
나는 이것이 클래스 java.util.Arrays 가이 메소드의 부작용으로로드되는 것을 피하는 것이라고 생각합니다. 애플리케이션 코드의 경우 일반적으로 문제가되지 않습니다.
이것은 테스트하기 가장 쉬운 것입니다. 이러한 프로그램을 컴파일 해 보겠습니다.
public class HashMapTest {
public static void main(String[] args) {
new java.util.HashMap();
}
}
그것을 실행합니다 java -verbose:class HashMapTest
. 클래스 로딩 이벤트가 발생할 때이를 인쇄합니다. JDK 1.8.0_60을 사용하면 400 개 이상의 클래스가로드 된 것을 볼 수 있습니다.
... 155 lines skipped ...
[Loaded java.util.Set from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.AbstractSet from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.Collections$EmptySet from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.Collections$EmptyList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.Collections$EmptyMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.Collections$UnmodifiableCollection from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.Collections$UnmodifiableList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.Collections$UnmodifiableRandomAccessList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded sun.reflect.Reflection from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
**[Loaded java.util.HashMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.HashMap$Node from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.lang.Class$3 from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.lang.Class$ReflectionData from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.lang.Class$Atomic from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded sun.reflect.generics.repository.AbstractRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded sun.reflect.generics.repository.GenericDeclRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded sun.reflect.generics.repository.ClassRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.lang.Class$AnnotationData from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded sun.reflect.annotation.AnnotationType from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.util.WeakHashMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.lang.ClassValue$ClassValueMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.lang.reflect.Modifier from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded sun.reflect.LangReflectAccess from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
[Loaded java.lang.reflect.ReflectAccess from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
**[Loaded java.util.Arrays from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar]
...
보시다시피 HashMap
는 애플리케이션 코드보다 오래 전에 Arrays
로드되고 HashMap
. HashMap
부하에 의해 트리거 sun.reflect.Reflection
가가로 초기화 HashMap
정적 필드를. Arrays
부하에 의해 촉발 될 가능성이 WeakHashMap
사실이 부하 Arrays.fill
에 clear()
방법. WeakHashMap
부하에 의해 트리거됩니다 java.lang.ClassValue$ClassValueMap
연장하는 WeakHashMap
. 는 ClassValueMap
매에 존재 java.lang.Class
인스턴스. 그래서 나에게 Arrays
클래스 가 없으면 JDK를 전혀 초기화 할 수없는 것 같습니다 . 또한 Arrays
정적 이니셜 라이저는 매우 짧으며 어설 션 메커니즘 만 초기화합니다. 이 메커니즘은 다른 많은 클래스에서 사용됩니다 (예 :java.lang.Throwable
매우 일찍로드 됨). 에서는 다른 정적 초기화 단계가 수행되지 않습니다 java.util.Arrays
. 따라서 @Holger 버전은 나에게 잘못된 것 같습니다.
여기서 우리는 또한 매우 흥미로운 것을 발견했습니다. 는 WeakHashMap.clear()
여전히 사용합니다 Arrays.fill
. 그것이 거기에 나타 났을 때 흥미롭지 만 불행히도 이것은 선사 시대 로 진행됩니다 (이미 최초의 공개 OpenJDK 저장소에있었습니다).
다음으로 @MarcoTopolnik 은 다음과 같이 말합니다 .
안전하지는 않지만 통화가 인라인되지 않고 짧을 때 더 빠를 수 있습니다 . HotSpot에서 루프와 명시 적 호출 모두 빠른 컴파일러 내장 (행복한 시나리오에서)이됩니다.
fill
tab
fill
Arrays.fill
직접적으로 내재화 되지 않은 것은 실제로 놀랍습니다 ( @apangin에 의해 생성 된 내장 목록 참조 ). 이러한 루프는 명시적인 고유 처리없이 JVM에서 인식하고 벡터화 할 수 있습니다. 따라서 매우 특정한 경우 (예 : 제한에 도달 한 경우)에서 추가 호출을 인라인 할 수 없다는 것은 사실입니다 . 반면에 매우 드문 상황이며 단일 호출이고 루프 내부 호출이 아니며 가상 / 인터페이스 호출이 아닌 정적이므로 성능 향상은 미미하고 일부 특정 시나리오에서만 발생할 수 있습니다. JVM 개발자가 일반적으로 신경 쓰는 것은 아닙니다.MaxInlineLevel
또한 C1 '클라이언트'컴파일러 (티어 1-3)조차도 inlining log ( )가 말한 것처럼 Arrays.fill
in WeakHashMap.clear()
과 같이 인라인 할 수 있습니다 -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining
.
36 3 java.util.WeakHashMap::clear (50 bytes)
!m @ 4 java.lang.ref.ReferenceQueue::poll (28 bytes)
@ 17 java.lang.ref.ReferenceQueue::reallyPoll (66 bytes) callee is too large
@ 28 java.util.Arrays::fill (21 bytes)
!m @ 40 java.lang.ref.ReferenceQueue::poll (28 bytes)
@ 17 java.lang.ref.ReferenceQueue::reallyPoll (66 bytes) callee is too large
@ 1 java.util.AbstractMap::<init> (5 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 9 java.lang.ref.ReferenceQueue::<init> (27 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 10 java.lang.ref.ReferenceQueue$Lock::<init> (5 bytes) unloaded signature classes
@ 62 java.lang.Float::isNaN (12 bytes) inline (hot)
@ 112 java.util.WeakHashMap::newTable (8 bytes) inline (hot)
물론 스마트하고 강력한 C2 '서버'컴파일러에 의해 쉽게 인라인됩니다. 따라서 여기서는 문제가 없습니다. @Marco 버전도 잘못된 것 같습니다.
마지막으로 @StuartMarks (JDK 개발자이므로 공식 목소리)로부터 몇 가지 의견이 있습니다.
흥미 롭군. 내 직감은 이것이 실수라는 것입니다. 이 변경 세트에 대한 검토 스레드는 여기에 있으며 여기에서 계속되는 이전 스레드 를 참조합니다 . 이전 스레드의 초기 메시지는 Doug Lea의 CVS 저장소에있는 HashMap.java의 프로토 타입을 가리 킵니다. 이게 어디서 왔는지 모르겠어요. OpenJDK 역사의 어떤 것과도 일치하지 않는 것 같습니다.
... 어쨌든 오래된 스냅 샷일 수 있습니다. for-loop는 수년 동안 clear () 메서드에있었습니다. Arrays.fill () 호출은 이 변경 세트에 의해 도입 되었으므로 몇 달 동안 만 트리에있었습니다. 또한 이 변경 세트에 의해 도입 된 Integer.highestOneBit () 기반 2의 거듭 제곱 계산 도 동시에 사라졌지 만, 검토 중에 언급되었지만 취소되었습니다. 흠.
실제로 HashMap.clear()
포함 된 루프는 수년 동안 포함되었으며 2013 년 4 월 10 일에 교체 되었으며 Arrays.fill
논의 된 커밋 이 도입 된 9 월 4 일까지 1 년 반 이 짧았습니다. 논의 된 커밋은 실제로 JDK-8023463 문제 HashMap
를 수정하기 위해 내부를 대대적으로 재 작성한 것입니다 . 중복 된 해시 코드 가 있는 키를 사용하여 검색 속도를 선형으로 줄여 DoS 공격에 취약하게 만들 가능성에 대한 긴 이야기였습니다 . 이 문제를 해결하려는 시도는 String hashCode의 일부 무작위 화를 포함하여 JDK-7에서 수행되었습니다. 그래서HashMap
HashMap
HashMap
구현은 이전 커밋에서 분기되어 독립적으로 개발 된 다음 마스터 브랜치로 병합되어 그 사이에 도입 된 몇 가지 변경 사항을 덮어 씁니다.
우리는 diff를 수행하는이 가설을지지 할 수 있습니다. 테이크 버전Arrays.fill
제거 (2013년 9월 4일을)과와 비교 이전 버전 (2013년 7월 30일). diff -U0
출력 4,341 라인을 갖는다. 이제 추가 된 이전 버전Arrays.fill
(2013-04-01) 과 비교해 보겠습니다 . 이제 diff -U0
2680 줄만 포함됩니다. 따라서 최신 버전은 실제로 직계 부모보다 이전 버전과 더 유사합니다.
결론
결론적으로 저는 Stuart Marks에 동의합니다. 을 제거 할 구체적인 이유가 없었 Arrays.fill
습니다. 이는 중간 변경 내용이 실수로 덮어 써 졌기 때문입니다. 사용 Arrays.fill
은 JDK 코드와 사용자 응용 프로그램 모두에서 완벽하게 괜찮으며 예를 들어 WeakHashMap
. Arrays
클래스는 꽤 일찍 JDK를 초기화하는 동안 어쨌든로드, 아주 간단한 정적 초기화하고있다 Arrays.fill
성능에 단점이 관찰되지해야한다, 그래서 방법은 쉽게, 심지어 클라이언트 컴파일러에 의해 인라인 될 수 있습니다.
훨씬 빠르기 때문에 !
두 가지 방법의 축소 버전에 대해 철저한 벤치마킹 테스트를 실행했습니다.
void jdk7clear() {
Arrays.fill(table, null);
}
void jdk8clear() {
Object[] tab;
if ((tab = table) != null) {
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
임의의 값을 포함하는 다양한 크기의 배열에서 작동합니다. (일반적인) 결과는 다음과 같습니다.
Map size | JDK 7 (sd)| JDK 8 (sd)| JDK 8 vs 7
16| 2267 (36)| 1521 (22)| 67%
64| 3781 (63)| 1434 ( 8)| 38%
256| 3092 (72)| 1620 (24)| 52%
1024| 4009 (38)| 2182 (19)| 54%
4096| 8622 (11)| 4732 (26)| 55%
16384| 27478 ( 7)| 12186 ( 8)| 44%
65536| 104587 ( 9)| 46158 ( 6)| 44%
262144| 445302 ( 7)| 183970 ( 8)| 41%
And here are the results when operating over an array filled with nulls (so garbage collection issues are eradicated):
Map size | JDK 7 (sd)| JDK 8 (sd)| JDK 8 vs 7
16| 75 (15)| 65 (10)| 87%
64| 116 (34)| 90 (15)| 78%
256| 246 (36)| 191 (20)| 78%
1024| 751 (40)| 562 (20)| 75%
4096| 2857 (44)| 2105 (21)| 74%
16384| 13086 (51)| 8837 (19)| 68%
65536| 52940 (53)| 36080 (16)| 68%
262144| 225727 (48)| 155981 (12)| 69%
The numbers are in nanoseconds, (sd)
is 1 standard deviation expressed as a percentage of the result (fyi, a "normally distributed" population has an SD of 68), vs
is the JDK 8 timing relative to JDK 7.
It is interesting that not only is it significantly faster, but the deviation is also slightly narrower, which means that the JDK 8 implementation gives slightly more consistent performance.
The tests were run on jdk 1.8.0_45 over a large (millions) number of times on arrays populated with random Integer
objects. To remove out-lying numbers, on each set of results the fastest and slowest 3% of timings were discarded. Garbage collection was requested and the thread yielded and slept just prior to running each invocation of the method. JVM warm up was done on the first 20% of work and those results were discarded.
For me, the reason is a likely performance inprovement, at a negligible cost in terms of code clarity.
Note that the implementation of the fill
method is trivial, a simple for-loop setting each array element to null. So, replacing a call to it with the actual implementation does not cause any significant degradation in the clarity/conciseness of the caller method.
The potential performance benefits are not so insignificant, if you consider everything that is involved:
There will be no need for the JVM to resolve the
Arrays
class, plus loading and initializing it if needed. This is a non-trivial process where the JVM performs several steps. Firstly, it checks the class loader to see if the class is already loaded, and this happens every time a method is called; there are optimizations involved here, of course, but it still takes some effort. If the class is not loaded, the JVM will need to go through the expensive process of loading it, verifying the bytecode, resolving other necessary dependencies, and finally performing static initialization of the class (which can be arbitrarily expensive). Given thatHashMap
is such a core class, and thatArrays
is such a huge class (3600+ lines), avoiding these costs may add up to noticeable savings.Since there is no
Arrays.fill(...)
method call, the JVM won't have to decide whether/when to inline the method into the caller's body. SinceHashMap#clear()
tends to get called a lot, the JVM will eventually perform the inlining, which requires JIT recompilation of theclear
method. With no method calls,clear
will always run at top-speed (once initially JITed).
Another benefit of no longer calling methods in Arrays
is that it simplifies the dependency graph inside the java.util
package, since one dependency is removed.
I'm going to shoot in the dark here...
My guess is that it might have been changed in order to prepare the ground for Specialization (aka generics over primitive types). Maybe (and I insist on maybe), this change is meant to make transition to Java 10 easier, in the event of specialization being part of the JDK.
If you look at the State of the Specialization document, Language restrictions section, it says the following:
Because any type variables can take on value as well as reference types, the type checking rules involving such type variables (henceforth, "avars"). For example, for an avar T:
- Cannot convert null to a variable whose type is T
- Cannot compare T to null
- Cannot convert T to Object
- Cannot convert T[] to Object[]
- ...
(Emphasis is mine).
And ahead in the Specializer transformations section, it says:
When specializing an any-generic class, the specializer is going to perform a number of transformations, most localized, but some requiring a global view of a class or method, including:
- ...
- Type variable substitution and name mangling is performed on the signatures of all methods
- ...
Later on, near the end of the document, in the Further investigation section, it says:
While our experiments have proven that specialization in this manner is practical, much more investigation is needed. Specifically, we need to perform a number of targeted experiments aimed at any-fying core JDK libraries, specifically Collections and Streams.
Now, regarding the change...
If the Arrays.fill(Object[] array, Object value)
method is going to be specialized, then its signature should change to Arrays.fill(T[] array, T value)
. However this case is specifically listed in the (already mentioned) Language restrictions section (it would violate the emphasized items). So maybe someone decided that it would be better to not use it from the HashMap.clear()
method, especially if value
is null
.
There is no actual difference in the functionality between the 2 version's loop. Arrays.fill
does the exact same thing.
So the choice to use it or not may not necessarily be considered a mistake. It is left up to the developer to decide when it comes to this kind of micromanagement.
There are 2 separate concerns for each approach:
- using the
Arrays.fill
makes the code less verbose and more readable. - looping directly in the
HashMap
code (like version 8) peformance wise is actually a better option. While the overhead that inserting theArrays
class is negligible it may become less so when it comes to something as widespread asHashMap
where every bit of performance enhancement has a large effect(imagine the tiniest footprint reduce of a HashMap in fullblown webapp). Take into consideration the fact that the Arrays class was used only for this one loop. The change is small enough that it doesn't make the clear method less readable.
The precise reason can't be found out without asking the developer who actually did this, however i suspect it's either a mistake or a small enhancement. better option.
My opinion is it can be considered an enhancement, even if only by accident.
참고URL : https://stackoverflow.com/questions/32693704/why-is-arrays-fill-not-used-in-hashmap-clear-anymore
'Nice programing' 카테고리의 다른 글
g ++로 컴파일되는 이상한 코드 (0) | 2020.11.02 |
---|---|
영화 상영 시간 API가 있나요? (0) | 2020.11.02 |
OS X의 경로에서 / usr / bin 전에 / usr / local / bin을 갖는 데 문제가 있습니까? (0) | 2020.11.02 |
완전한 Cocos2d-x 튜토리얼 및 가이드 목록 (0) | 2020.11.02 |
스크롤 이벤트가 사용자에 의해 생성되었는지 감지 (0) | 2020.11.02 |