Kotlin의 범위.for Each 느려요?

10698 단어 Kotlin
얼마 전 《Kotlin 잠재적 비용에 대한 기준》라는 제목의 글이 화제가 되었다.
그 중에서 Kotlin을 사용한Range의 for순환과forEach()의 비교가 있고forEach()는'절대 피하는 것이 좋다'는 결론을 얻었다.이 점과 Kotlin's hidden costs - Benchmarks 등에 대한 반응이 궁금하다.
그래서 이번에 우리는 Range의 for순환을 대상으로 조사했다.

기사를 다시 한 번 살펴보겠습니다.


원래 보도에는 기준 결과가 적혀 있는데, 수치는 다음과 같다.
Benchmark                                                                             Mode   Samples         Mean   Mean error    Units
c.a.k.part3.KotlinBenchmarkPart3.kotlinRangeForEachFunction                          thrpt       200   108382.188      561.632   ops/ms
c.a.k.part3.KotlinBenchmarkPart3.kotlinRangeForEachLoop                              thrpt       200   331558.172      494.281   ops/ms
c.a.k.part3.KotlinBenchmarkPart3.kotlinRangeForEachLoopWithStep1                     thrpt       200   331250.339      545.200   ops/ms
확실히 사용forEach의 기준kotlinRangeForEachFunction은 다른 것보다 3배가 걸린다.하지만 그뿐인데 정말'절대 피하는 게 좋다'고 말할 수 있는 차이일까.
결과를 잘 봅시다.단위는 모두ops/ms입니다.이를 바탕으로'한 번의 처리에 초가 걸렸는가'를 계산하면 다음과 같다.
이름
컨텐트
분수
kotlinRangeForEachFunction
forEach() 호출
9.227(ns/op)
kotlinRangeForEachLoop
for...in 순환
3.016(ns/op)
kotlinRangeForEachLoopWithStep1
for...in...by 순환
3.019(ns/op)
여기서 진행되는 처리는 1...10의 처리.즉, 이'3배의 차이'는 10회 순환에 9나초가 걸리느냐, 3나초가 다르느냐는 것이다.
참고로 원래 기준은 코어 i7의 맥북 프로를 사용한 것 같지만 시도해 본 사람도 있다.for 순환은 170나초 정도이고 forEach() 사용하면 약 3배인 500나초 정도가 걸린다.
도대체 이 격차가 얼마나 큰 영향을 미칠까.

다른 프로세스와 비교


출납초 수준의 숫자를 줘도 아직 실감이 나지 않을 수도 있다.그래서 나는 처리를 조금 바꾸면서 비교를 했다.
또한 측정 대상의 기계는 다음과 같다.
  • 모델... Nexus5x
  • CPU... Qualcomm Snapdragon 808 1.8GHz(2코어) +1.4GHz(4코어)
  • 메모리... 2GB
  • OS... Android의 8.0.0
  • Kotlin 범위 순환


    원시 기준Range의 for순환과 forEach() 함수를 사용하여 처리했습니다.forEach() 호출t0_range_forEachFunction
    fun rangeForEachMethod(blackHole: BlackHole) {
        (1..10).forEach {
            blackHole.consume(it)
        }
    }
    
    for...in 순환t0_range_forEachLoop
    fun rangeForEachLoop(blackHole: BlackHole) {
        for (it in 1..10) {
            blackHole.consume(it)
        }
    }
    

    결과


    Android 7.1.2 Pixel 기준
    역시 forEach() 호출이 느려서 6~7배의 시간이 걸린다.그런데 아까 Android 기준보다 100초 정도 빨랐어요.

    순환 중 추가 처리


    현실에서는 빈 처리를 하지 않기 때문에 다른 처리를 포함시켜 보자.
    원소를 생성하는 Int수 그룹t1_rangeForEachLoop_withArrayCreation
    fun rangeForEachLoopWithArrayCreation(blackHole: BlackHole) {
        for (it in 1..10) {
            blackHole.consumeArray(intArrayOf(it))
        }
    }
    
    Int? 역할 할당t1_rangeForEachLoop_withBoxing
    fun rangeForEachLoopWithBoxing(blackHole: BlackHole) {
        for (it in 1..10) {
            blackHole.consume(it as Int?)
        }
    }
    
    Optional로 변환하면 느리게 인식되는 자동 부트 수행
    큰 숫자Int?에 역할 분배t1_rangeForEachLoop_withBoxingLargeNum
    fun rangeForEachLoopWithBoxingLargeNum(blackHole: BlackHole) {
        for (it in 1001..1010) {
            blackHole.consume(it as Int?)
        }
    }
    
    Java는 최대 127개의 정형 자동 부트를 최적화합니다.더 큰 수량은 매번 생성Integer, 심지어 더 느리다.
    무작위 액세스 그룹 t1_rangeForEachLoop_withRandomAccess
    fun rangeForEachLoopWithRandomAccess(blackHole: BlackHole, ra: RandomAccessor) {
        for (it in 1..10) {
            blackHole.consume(ra.next(it))
        }
    }
    
    ra.next() 그룹 요소에 한 번만 접근하도록 호출합니다.몇 번째 방문은 무작위입니다.최대 100만 개의 요소를 구성하여 CPU에 캐시 액세스가 불가능합니다.

    결과



    주의해야 할 것은 무작위 방문에 걸리는 시간이죠.그것은 원래의 for 순환보다 약 35배의 시간을 썼다.축과 수조를 자동으로 생성하는 데 걸리는 시간이 훨씬 많다.
    이것은 메모리가 CPU 캐시에 저장되지 않은 원인이라고 할 수 있다.일반적으로 캐시를 타지 않는 상황에서 100배 늦는 것은 그리 신기한 일이 아니다.그럼에도 불구하고 지연 시간은 매번 200나노초 안팎이라는 점에서 주목할 만하다.
    자동 안내나 수조 생성이 있으면 forEach() 과 같은 시간이 필요하기 때문에 forEach() 최적화 전에 대상이 생성되었는지 확인하는 것이 좋다.

    순환을 위한 Java


    원래 데이텀에는 Java 루프 데이텀이 없습니다.참고로 Java 순환을 추가하고 다른 처리forEach() 함수와 Range for 순환과 비교하고 추가했습니다.

    결과



    비록 약간 작지만 10번의 순환은 몇 십 나초 정도의 차이로 오차의 범위이다.

    내장 확장


    순환 자체의 원가 시간을 제외하고 상기 과정은 호출 시간blackHole.consume(…)을 포함한다.이 함수는 과도한 최적화를 방지하는 가상 처리 (이 처리가 없으면 빈 순환 등 순환 자체가 삭제되어 측정할 수 없음) 라고 쓰여 있다.
    이 가상의 처리에 얼마나 많은 시간이 걸렸는지 보기 위해 우리는 10차례의 순환을 전개하여 측정을 진행했다.이 밖에 1회/2회/4회 순환이 추가되었습니다.

    결과



    시간이 걸리는 것은 for 순환과 큰 변화가 없다.지금까지 진행된 기준에서 50나초 정도는 blackHole.consume(…) 호출에 달려 있다는 얘기다.특히 for 순환을 사용하는 대부분의 처리는 이 처리가 차지한다.

    총결산


    그 기준의 기사를 보고 "절대 피해야 한다forEach()""그럴 이유가 있나!"토하고 싶은 건 나뿐만이 아니지?
    KotlinRange의 for를 사용하면 순환이 매우 빨라서 병목이 되지 않는다고 할 수 있다.또한 Kotlin의 forEach() 순환은 상대적으로 느리다고 할 수 있지만 순환 내의 처리가 매우 간단하고 대량으로 호출되는 경우를 제외하고는 그 영향이 뚜렷하지 않다.
    또 이런 미기준은 당연히 효과가 있지만 대추를 통째로 삼키는 것도 위험하다.이번 예에서 측정 대상과 무관blackHole.consume(…)의 처리는 기준 결과에 큰 영향을 미쳤다.(또 원래 안드로이드를 향한 기준blackHole에서 부정확한 결과를 초래한 것으로 여겨져 크게 수정했다.이번 기준 결과가 조금 빨라진 것은 이 때문이다)
    참고로 이번 기준 프로그램은 다음과 같은 내용을 공개했다.
    마지막으로, 나는 메모리 접근이 일부 연산 처리와 함수 호출보다 더 오래 걸린다고 생각한다.그나저나 얼마 전 참고할 만한 트위터와 자료가 화제가 됐으니 링크를 붙여야 한다.
    이 시계는 괜찮다.하드 드라이브는 매우 느리다 (대륙 전역과 겨우 한 자릿수 차이).https://t.co/w5W0bLTyge-TT@홋카이도(@edy555)pic.twitter.com/e2TccGVp7q
    출처로 여겨지는 자료(영어)
    2017년 8월 11일.

    좋은 웹페이지 즐겨찾기