C / C ++와 비교하여 Java가 성능을 "조정"하기가 훨씬 더 어려워 집니까? [닫은]


11

JVM의 "마법"이 프로그래머가 Java에서 마이크로 최적화에 미치는 영향을 방해합니까? 나는 최근에 C ++에서 때때로 데이터 멤버의 순서가 최적화 (마이크로 초 환경에서 부여)를 제공 할 수 있으며 Java의 성능을 압박 할 때 프로그래머의 손이 묶여 있다고 가정 했습니까?

괜찮은 알고리즘이 더 빠른 속도 향상을 제공한다고 생각하지만, 올바른 알고리즘이 있으면 JVM 제어로 인해 Java를 조정하기가 더 어렵습니까?

그렇지 않다면 사람들은 Java에서 사용할 수있는 트릭 (간단한 컴파일러 플래그 외에도)에 대한 예를 제공 할 수 있습니다.


14
모든 Java 최적화의 기본 원칙은 다음과 같습니다. JVM이 이미 가능한 것보다 더 잘 수행했을 것입니다. 최적화는 주로 합리적인 프로그래밍 방식을 따르고 루프에서 문자열을 연결하는 것과 같은 일반적인 것을 피합니다.
Robert Harvey

3
모든 언어 에서 마이크로 최적화의 원칙은 컴파일러가 이미 할 수있는 것보다 더 잘 수행했다는 것입니다. 모든 언어에서 마이크로 최적화의 다른 원리는 하드웨어를 더 많이 넣는 것이 프로그래머의 마이크로 최적화 시간보다 저렴하다는 것입니다. 프로그래머는 문제를 조정하는 경향이 있지만 (최적화 알고리즘) 마이크로 최적화는 시간 낭비입니다. 마이크로 최적화는 하드웨어를 더 많이 넣을 수없는 임베디드 시스템에서 의미가 있지만 Java를 사용하는 Android 및 다소 구현이 좋지 않은 경우 대부분의 하드웨어가 이미 충분하다는 것을 보여줍니다.
Jan Hudec

1
"자바 성능 속임수"에 대한 가치 연구는 다음과 같습니다 효과적인 자바 , 안젤리카 랭거 링크 - 자바 성능 과 브라이언 게츠하여 성능 관련 기사 자바 이론과 실습스레딩 살짝 시리즈 나열 여기
모기

2
에 JVM, 운영 체제 및 하드웨어 이동 - - 당신은 성능 튜닝 방법론을 학습에 대한 개선 사항을 적용 해제 최고야 팁과 트릭에 대해 특히주의 하여 :-) 특정 환경
마티 Verburg

경우에 따라 VM은 런타임시 최적화를 수행하여 컴파일 타임에 수행하기가 비실용적 일 수 있습니다. 관리되는 메모리를 사용하면 성능을 향상시킬 수 있지만 종종 메모리 공간이 더 많습니다. 사용하지 않은 메모리는 최대한 빨리 가능한 경우 사용 가능합니다.
Brian

답변:


5

물론 마이크로 최적화 수준에서 JVM은 특히 C 및 C ++에 비해 거의 제어 할 수없는 몇 가지 작업을 수행합니다.

반면에 C 및 C ++의 다양한 컴파일러 동작은 모든 종류의 모호한 방식으로 (컴파일러 개정판에서도) 미세 최적화를 수행하는 기능에 훨씬 부정적인 영향을 미칩니다.

어떤 종류의 프로젝트를 조정하고 어떤 환경을 대상으로하는지 등에 따라 다릅니다. 그리고 결국 알고리즘 / 데이터 구조 / 프로그램 설계 최적화로부터 몇 배나 더 나은 결과를 얻을 수 있기 때문에 실제로 중요하지 않습니다.


앱이 여러 코어에서 확장되지 않는 것을 발견하면 큰 문제가 될 수 있습니다.
James

@ 제임스-정교한 관리?
Telastyn

1
시작하려면 여기를 참조하십시오 : mechanical-sympathy.blogspot.co.uk/2011/07/false-sharing.html
James

1
@James의 핵심 확장은 구현 언어 (Python 제외)와 거의 관련이 없으며 애플리케이션 아키텍처와 더 관련이 있습니다.
James Anderson

29

마이크로 최적화는 거의 시간 가치가 없으며 거의 ​​모든 쉬운 것은 컴파일러와 런타임에 의해 자동으로 수행됩니다.

그러나 C ++과 Java가 근본적으로 다른 최적화의 한 가지 중요한 영역이 있으며 이는 대량 메모리 액세스입니다. C ++에는 수동 메모리 관리 기능이있어 애플리케이션의 데이터 레이아웃 및 액세스 패턴을 최적화하여 캐시를 최대한 활용할 수 있습니다. 이는 실제로 실행중인 하드웨어에 따라 다소 다르지만 (다른 하드웨어에서는 성능 향상이 사라질 수 있음) 제대로 수행하면 성능이 크게 향상 될 수 있습니다. 물론 모든 종류의 끔찍한 벌레에 대한 가능성으로 지불합니다.

Java와 같은 가비지 수집 언어를 사용하면 이러한 종류의 최적화를 코드에서 수행 할 수 없습니다. 일부는 런타임 (자동 또는 구성, 아래 참조)으로 수행 할 수 있으며 일부는 불가능합니다 (메모리 관리 버그로부터 보호하기 위해 지불하는 가격).

그렇지 않다면 사람들은 Java에서 사용할 수있는 트릭 (간단한 컴파일러 플래그 외에도)에 대한 예를 제공 할 수 있습니다.

Java 컴파일러는 최적화가 거의 없기 때문에 컴파일러 플래그는 Java와 관련이 없습니다. 런타임은 않습니다.

그리고 실제로 Java 런타임에는 특히 가비지 수집기와 관련하여 조정할 수 있는 많은 매개 변수 가 있습니다. 이러한 옵션에 대해 "간단한"것은 없습니다. 기본값은 대부분의 응용 프로그램에 적합하며 더 나은 성능을 얻으려면 옵션의 기능과 응용 프로그램의 동작을 정확하게 이해해야합니다.


1
+1 : 기본적으로 내가 답에 쓴 내용, 더 나은 공식화.
Klaim

1
+1 : 매우 간결한 방법으로 설명 된 매우 좋은 점 : "이것은 매우 어렵지만 ... 올 바르면 놀라운 성능을 발휘할 수 있습니다. 물론 모든 종류의 끔찍한 버그에 대한 가능성으로 지불합니다. "
Giorgio

1
@MartinBa : 메모리 관리 최적화 비용이 더 많이 듭니다. 메모리 관리를 최적화하지 않으려는 경우 C ++ 메모리 관리는 그렇게 어렵지 않습니다 (STL을 통해 완전히 피하거나 RAII를 사용하여 비교적 쉽게 수행). 물론, C ++에서 RAII를 구현하려면 Java에서 아무것도하지 않는 것보다 더 많은 코드가 필요합니다 (즉, Java가이를 처리하기 때문에).
Brian

3
@Martin Ba : 기본적으로 그렇습니다. 매달려있는 포인터, 버퍼 오버플로, 초기화되지 않은 포인터, 포인터 산술 오류, 수동 메모리 관리 없이는 존재하지 않는 모든 것. 메모리 액세스를 최적화하려면 많은 수동 메모리 관리 가 필요합니다 .
Michael Borgwardt

1
자바에서 할 수있는 일이 몇 가지 있습니다. 하나는 객체 풀링으로 객체의 메모리 지역성을 극대화합니다 (메모리 지역성을 보장 할 수있는 C ++와 달리).
RokL

5

[...] (마이크로 초 환경에서 부여됨) [...]

우리가 수백만에서 수십억 개의 것들을 반복한다면 마이크로 초가 더해진다. C ++의 개인 vtune / 마이크로 최적화 세션 (알고리즘 개선 사항 없음) :

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

"멀티 스레딩", "SIMD"(컴파일러를 이길 수 있도록 손으로 쓴 것) 및 4 가의 패치 최적화 이외의 모든 것은 마이크로 레벨 메모리 최적화였습니다. 또한 32 초의 초기 시간부터 시작하는 원래 코드는 이미 상당히 최적화되어 있습니다 (이론적으로 최적의 알고리즘 복잡성). 이것은 최근 세션입니다. 이 최근 세션 이전의 원래 버전은 처리하는 데 5 분이 걸렸습니다.

메모리 효율성을 최적화하면 단일 스레드 컨텍스트 및 여러 스레드 컨텍스트에서 여러 번에서 수십 배까지 도움이 될 수 있습니다.

미세 최적화의 중요성

마이크로 최적화는 시간 낭비라는이 아이디어에 약간 흥분합니다. 나는 이것이 일반적인 조언이지만, 모든 사람들이 측정보다는 허치와 미신에 기초하여 잘못하지는 않는다는 데 동의합니다. 올바르게 수행하면 반드시 미세한 영향을 줄 필요는 없습니다. 인텔 자체의 Embree (레이트 레이싱 커널)를 가져 와서 작성한 스칼라 BVH (지수 적으로 이길 수없는 레이 패킷이 아님) 만 테스트 한 다음 해당 데이터 구조의 성능을 이길 수 있다면 수십 년 동안 코드를 프로파일 링하고 튜닝하는 데 사용 된 베테랑에게도 겸손한 경험. 그리고 그것은 모두 미세 최적화가 적용 되었기 때문입니다. 산업 전문가들이 레이트 레이싱 작업을하는 것을 보았을 때 그들의 솔루션은 초당 1 억 개 이상의 광선을 처리 할 수 ​​있습니다.

알고리즘 중심으로 BVH를 간단하게 구현하고 최적화 컴파일러 (Intel 자체 ICC)에 대해 초당 1 억 개 이상의 1 차 광선 교차점을 얻을 수있는 방법은 없습니다. 간단한 것은 종종 초당 백만 개의 광선을 얻지 못합니다. 초당 수백만 개의 광선을 얻기 위해서는 전문가 수준의 솔루션이 필요합니다. 초당 1 억 개 이상의 광선을 얻으려면 인텔 수준의 미세 최적화가 필요합니다.

알고리즘

몇 분에서 몇 초 또는 몇 분에서 몇 초 정도의 성능이 중요하지 않은 한 마이크로 최적화는 중요하지 않다고 생각합니다. 버블 정렬과 같은 끔찍한 알고리즘을 가져 와서 예를 들어 대량 입력에 사용하고 병합 정렬의 기본 구현과 비교하면 전자는 처리하는 데 몇 개월이 걸릴 수 있으며 후자는 12 분이 걸릴 수 있습니다 이차 대 선형의 복잡성.

몇 달과 몇 분의 차이는 아마도 성능이 중요한 분야에서 일하지 않는 사람들조차도 대부분의 사람들이 결과를 얻기 위해 몇 달을 기다려야하는 경우 실행 시간을 받아 들일 수없는 것으로 간주하게 될 것입니다.

한편, 마이크로 최적화되지 않은 간단한 병합 정렬을 퀵 정렬과 비교하면 (합병 정렬보다 알고리즘 적으로 우수하지 않으며, 참조 지역에 대한 마이크로 레벨 개선 만 제공), 마이크로 최적화 된 퀵 정렬은 다음과 같이 끝날 수 있습니다. 12 분이 아닌 15 초 사용자가 12 분 동안 기다리게하는 것은 완벽하게 받아 들일 수 있습니다 (커피 브레이크 시간).

12 분에서 15 초 사이에 대부분의 사람들에게이 차이는 무시할 수 있다고 생각합니다. 그래서 마이크로 최적화는 종종 몇 분과 몇 달이 아닌 몇 분과 몇 초의 차이와 비슷하기 때문에 종종 쓸모없는 것으로 간주됩니다. 그것이 쓸모없는 것으로 간주되는 다른 이유는 그것이 중요하지 않은 영역에 자주 적용되기 때문입니다 : 루프가없고 중요하지 않은 작은 영역은 의심할만한 1 % 차이를 유발합니다 (소음 일 수도 있음). 그러나 이러한 유형의 시간 차이에 관심이 있고 측정하고 올바르게 기꺼이하는 사람들에게는 적어도 메모리 계층 구조의 기본 개념 (특히 페이지 결함 및 캐시 누락과 관련된 상위 수준)에주의를 기울일 가치가 있다고 생각합니다. .

자바, 마이크로 최적화를위한 충분한 공간 확보

휴, 죄송합니다.

JVM의 "마법"이 프로그래머가 Java에서 마이크로 최적화에 미치는 영향을 방해합니까?

당신이 올바르게하면 사람들이 생각할 수있는만큼 조금은 아닙니다. 예를 들어, 필기 처리 된 SIMD, 멀티 스레딩 및 메모리 최적화 (이미지 처리 알고리즘에 따라 액세스 패턴 및 가능한 표현)를 사용하여 원시 코드에서 이미지 처리를 수행하는 경우 32- 초 동안 초당 수억 개의 픽셀을 쉽게 처리 할 수 ​​있습니다. 비트 RGBA 픽셀 (8 비트 컬러 채널) 및 때로는 초당 수십억 개.

당신이 말하면, Pixel객체를 만들면 Java의 어느 곳에서나 접근 할 수 없습니다 (이것만으로도 64 비트에서 픽셀 크기가 4 바이트에서 16으로 팽창합니다).

그러나 Pixel객체 를 피하고 바이트 배열을 사용하고 Image객체를 모델링하면 훨씬 더 가까워 질 수 있습니다 . 평범한 오래된 데이터 배열을 사용하기 시작하면 Java는 여전히 유능합니다. Java에서 이전에 이런 종류의 것들을 시도해 보았지만 평소보다 4 배 더 큰 사소한 작은 객체를 만들지 않고 (예 : 대신 사용 ) 대량 인터페이스를 모델링하기 시작 하면 상당히 감동 했습니다 . 인터페이스가 아닌 인터페이스. 심지어 객체가 아닌 평범한 오래된 데이터를 반복하는 경우 Java가 C ++ 성능과 경쟁 할 수 있다고 말하고 싶습니다 ( 예 : 의 거대한 배열 ).intIntegerImagePixelfloatFloat

메모리 크기보다 훨씬 더 중요한 것은 일련의 배열이 int연속적인 표현 을 보장한다는 것입니다. Integer하지 않는 배열 . 연속성 (contiguity)은 여러 요소 (예 : 16 ints)가 모두 단일 캐시 라인에 적합하고 효율적인 메모리 액세스 패턴으로 제거하기 전에 잠재적으로 함께 액세스 될 수 있기 때문에 참조의 로컬성에 필수적입니다 . 한편 단일 Integer메모리는 주변 메모리가 관련이없는 메모리 어딘가에 좌초 될 수 있으며, 16 개 정수가 아닌 제거 전에 단일 정수만 사용하기 위해 해당 메모리 영역 만 캐시 라인에로드해야합니다. 우리가 놀랍도록 운이 좋고 주변에 있어도Integers메모리에서 서로 바로 옆에 있었으므로 Integer4 배 더 큰 결과로 제거하기 전에 액세스 할 수있는 캐시 라인에 4 개만 넣을 수 있으며 , 이는 가장 좋은 시나리오입니다.

또한 동일한 메모리 아키텍처 / 계층 구조로 통합 된 이후 마이크로 최적화가 많이 이루어졌습니다. 메모리 액세스 패턴은 사용하는 언어에 관계없이 루프 타일링 / 블로킹과 같은 개념이 일반적으로 C 또는 C ++에서 훨씬 더 많이 적용될 수 있지만 Java에도 많은 이점이 있습니다.

나는 최근에 C ++에서 때때로 데이터 멤버의 순서가 최적화를 제공 할 수 있다고 읽었다 ...]

데이터 멤버의 순서는 일반적으로 Java에서 중요하지 않지만 대부분 좋은 것입니다. C 및 C ++에서 ABI 이유로 인해 데이터 멤버의 순서를 유지하는 것이 종종 중요하므로 컴파일러는이를 망칠 필요가 없습니다. 패딩시 메모리 낭비를 피하기 위해 데이터 멤버를 내림차순으로 정렬하는 것과 같은 작업을 수행하는 인간 개발자는 신중해야합니다. Java를 사용하면 JIT가 패딩을 최소화하면서 적절한 정렬을 보장하기 위해 즉시 멤버를 재정렬 할 수 있습니다. 따라서 일반적인 C 및 C ++ 프로그래머가 종종 열악한 메모리를 낭비하는 방식으로 자동화합니다 ( 메모리 낭비뿐만 아니라 AoS 구조 간의 보폭을 불필요하게 늘리고 더 많은 캐시 누락을 유발하여 속도를 낭비하는 경우가 많습니다). 그것' 패딩을 최소화하기 위해 필드를 재정렬하는 매우 로봇적인 일이므로 이상적으로는 인간이 다루지 않습니다. 사람이 최적의 배열을 알도록 요구하는 방식으로 필드 배열이 중요한 유일한 시점은 오브젝트가 64 바이트보다 크고 액세스 패턴 (최적의 패딩이 아님)을 기반으로 필드를 배열하는 경우입니다. 보다 인간적인 노력이 될 수 있습니다 (중요한 경로를 이해해야 함. 일부는 사용자가 소프트웨어로 무엇을할지 몰라도 컴파일러가 예측할 수없는 정보 임).

그렇지 않다면 사람들은 Java에서 사용할 수있는 트릭 (간단한 컴파일러 플래그 외에도)에 대한 예를 제공 할 수 있습니다.

Java와 C ++ 간의 최적화 정신 측면에서 가장 큰 차이점은 C ++을 사용하면 성능이 중요한 시나리오에서 Java보다 객체를 조금 더 사용할 수 있다는 것입니다. 예를 들어 C ++은 오버 헤드가없는 클래스에 정수를 랩핑 할 수 있습니다 (모든 곳에서 벤치 마크 됨). Java는 객체 당 메타 데이터 포인터 스타일 + 정렬 패딩 오버 헤드를 가져야하므로 Boolean보다 큽니다 boolean(그러나 반사의 균일 한 이점을 제공 final하고 모든 단일 UDT에 대해 표시되지 않은 기능을 무시하는 기능 ).

C ++에서는 비균질 필드에서 메모리 레이아웃의 연속성을 제어하는 ​​것이 조금 더 쉽습니다 (예 : 구조체 / 클래스를 통해 부동 소수점과 정수를 하나의 배열로 인터리빙). GC를 통해 객체를 할당 할 때 Java로

...하지만 종종 고성능 솔루션은 종종 어쨌든 솔루션을 분할하고 연속 된 일반 데이터 배열에 SoA 액세스 패턴을 사용합니다. 따라서 최고의 성능을 필요로하는 영역의 경우 Java와 C ++ 간의 메모리 레이아웃을 최적화하는 전략은 종종 동일하며, 자주 사용하는 컬렉션 스타일 인터페이스를 선호하는 작은 객체 지향 인터페이스를 철거해야합니다. 콜드 필드 분할, SoA 담당자 등. 동종이 아닌 AoSoA 담당자는 Java에서 불가능한 것처럼 보이지만 (원시 바이트 배열 또는 이와 유사한 것을 사용하지 않는 한) 경우 모두 드문 경우입니다.순차적 및 랜덤 액세스 패턴은 핫 필드에 대한 필드 유형의 혼합을 동시에 갖는 동시에 빠를 필요가있다. 필자가이 두 성능 간의 최적화 전략 (일반적인 수준에서)의 차이의 대부분은 최고 성능에 도달하는 경우에 약합니다.

같은 작은 물체와 많이 할 수가없는 - 당신은 단순히 "좋은"성능에 도달하는 경우의 차이는 더 상당히 달라질 Integerint특히이 제네릭과 상호 작용하는 방식과, 좀 더 피타의 수 있습니다 . 그것은 자바의 중심 최적화 대상으로 단지 구축 한 일반 데이터 구조에 조금 더 어렵다 그것을위한 작품 int, float등을하는 것은 그 더 크고 더 비싼 UDT 일을 피할 수 있지만, 종종 가장 성능이 중요한 영역은 자신의 데이터 구조를 손으로 롤링이 필요합니다 동안 어쨌든 매우 구체적인 목적을 위해 조정되었으므로 성능은 좋지만 성능은 향상되지 않는 코드에만 귀찮습니다.

개체 오버 헤드

Java 객체 오버 헤드 (메타 데이터 및 공간적 국소성 손실 및 초기 GC주기 후 일시적인 국소성 손실)는 수백만에 의해 일부 데이터 구조에 저장되어있는 실제 크기 (예 : intvs. Integer매우 연속적이고 매우 꽉 찬 루프에서 액세스됩니다. 이 주제에 대해 많은 감도가있는 것처럼 보이므로 이미지와 같은 큰 물체의 객체 오버 헤드에 대해 걱정하고 싶지 않고 실제로는 단일 픽셀과 같은 작은 물체 만 걱정하고 싶지는 않습니다.

누군가이 부분에 대해 의심이 든다면 백만 개의 무작위 ints대 백만 개의 임의의 합계를 합산하고 Integers이를 반복적으로 수행하는 Integers것 ( 초기 GC주기 후 메모리의 재구성)을 벤치 마크하는 것이 좋습니다 .

최고의 트릭 : 공간을 최적화 할 수있는 인터페이스 디자인

따라서 작은 객체 (예 : a Pixel, 4-vector, 4x4 matrix, a Particle, 심지어 Account작은 것만 있는 경우)에 무거운 부하를 처리하는 장소를 다루는 경우 궁극적 인 Java 트릭 필드)는 이러한 조그마한 물건에 객체를 사용하지 않고 평범한 오래된 데이터의 배열 (함께 연결되어 있음)을 사용하는 것입니다. 것처럼 다음 컬렉션 인터페이스가 개체 Image, ParticleSystem, Accounts, 개인 사람은 인덱스로 액세스 할 수있는 등의 행렬이나 벡터의 컬렉션, 예를 들어,이 또한 C와 C의 궁극적 인 디자인 트릭 ++ 중 하나이며, 이후 심지어 기본적인 오브젝트 오버 헤드없이 분리 된 메모리, 단일 입자 수준에서 인터페이스를 모델링하면 가장 효율적인 솔루션을 방지 할 수 있습니다.


1
대량의 성능 저하가 실제로 중요한 영역에서 압도적 인 최대 성능을 발휘할 가능성이 높다는 점을 고려할 때 좋은 성능을 얻는 이점을 쉽게 무시할 수는 없다고 생각합니다. 그리고 구조체 배열을 배열 구조체로 바꾸는 트릭은 원래 구조체 중 하나를 포함하는 모든 (또는 거의 모든) 값이 동시에 액세스 될 때 다소 고장납니다. BTW : 나는 당신이 오래된 게시물을 많이 발굴하고 자신의 좋은 답변, 때로는 좋은 답변을 추가하는 것을 본다 ;-)
Deduplicator

1
@Deduplicator 희망 나는 너무 부딪쳐서 사람들을 성가 시게하지 않습니다! 이건 조금 조그마한 몸매를 가지고 있습니다. 어쩌면 조금 개선해야 할 것 같습니다. SoA와 AoS는 종종 어려운 문제입니다 (순차 vs. 랜덤 액세스). 필자의 경우 순차적 액세스와 임의 액세스가 혼합되어 있기 때문에 어떤 것을 사용해야하는지 거의 알 수 없습니다. 내가 종종 배운 귀중한 교훈은 데이터 표현을하기에 충분한 공간을 남겨 두는 인터페이스를 디자인하는 것입니다.

1
글쎄, 나는 상황이 정말 느리기 때문에 눈치 notice 다. 그리고 나는 각자의 시간을 보냈습니다.
중복 제거기

user204677사라 졌는지 궁금 합니다. 좋은 답변입니다.
oligofren

3

한편으로는 마이크로 최적화와 알고리즘의 좋은 선택 사이에는 중간 영역이 있습니다.

그것은 일정한 요소 속도 향상의 영역이며, 수십 배를 산출 할 수 있습니다.
그렇게하는 방법은 남은 것이 거의 없을 때까지 처음 30 %, 남은 것의 20 %, 남은 것의 50 %, 몇 번의 반복 등 실행 시간의 전체 부분을 제거하는 것입니다.

작은 데모 스타일 프로그램에서는 이것을 볼 수 없습니다. 당신이 볼 수있는 곳은 많은 클래스 데이터 구조를 가진 큰 심각한 프로그램에 있으며, 호출 스택은 일반적으로 많은 층 깊이입니다. 속도 향상 기회를 찾는 좋은 방법 은 프로그램 상태의 임의 시간 샘플검사하는 것 입니다.

일반적으로 속도 향상은 다음과 같이 구성됩니다.

  • new오래된 오브젝트를 풀링하고 재사용하여 호출을 최소화

  • 실제로 필요한 것이 아니라 일반성을 위해 거기에서 수행되는 일을 인식하고,

  • 빅 동작이 동일하지만 실제로 사용 된 액세스 패턴을 활용하는 서로 다른 컬렉션 클래스를 사용하여 데이터 구조를 수정합니다.

  • 함수를 다시 호출하는 대신 함수 호출로 얻은 데이터 저장

  • 중복 된 데이터 구조간에 알림 이벤트와 완전히 일관성을 유지하려는 것이 아니라 일정량의 불일치를 허용합니다.

그러나 샘플을 가져 와서 먼저 문제가되는 것으로 표시되지 않은 상태에서 이러한 작업을 수행해서는 안됩니다.


2

Java (내가 아는 한)는 메모리의 변수 위치를 제어 할 수 없으므로 잘못된 공유 및 변수 정렬과 같은 것을 피하기가 더 어렵습니다 (사용하지 않는 여러 멤버로 클래스를 채울 수 있음). 내가 당신을 활용할 수 없다고 생각하는 또 다른 것은와 같은 명령어 mmpause이지만, 이것들은 CPU에 따라 다르므로 필요하다면 Java가 사용하는 언어가 아닐 수도 있습니다.

C / C ++의 유연성을 제공하지만 C / C ++의 위험 이있는 안전하지 않은 클래스 가 있습니다 .

JVM이 코드에 대해 생성 하는 어셈블리 코드 를 보는 데 도움이 될 수 있습니다.

이러한 종류의 세부 사항을 살펴 보는 Java 앱에 대해 읽으려면 LMAX에서 발표 한 Disruptor 코드를 참조하십시오 .


2

이 질문은 언어 구현에 의존하기 때문에 대답하기가 매우 어렵습니다.

일반적으로 요즘에는 이러한 "마이크로 최적화"를위한 공간이 거의 없습니다. 주된 이유는 컴파일러가 컴파일 중에 이러한 최적화를 활용하기 때문입니다. 예를 들어 의미가 동일한 상황에서 사전 증분 및 사후 증분 연산자간에 성능 차이가 없습니다. 또 다른 예는 예를 들어 이것을 for(int i=0; i<vec.size(); i++)호출하는 대신size()각 반복 동안 멤버 함수는 루프 전에 벡터의 크기를 얻은 다음 단일 변수와 비교하여 반복 당 호출을 피하는 것이 좋습니다. 그러나 컴파일러가이 어리석은 경우를 감지하고 결과를 캐시하는 경우가 있습니다. 그러나 이것은 함수에 부작용이없는 경우에만 가능하며 컴파일러는 루프 동안 벡터 크기가 일정하게 유지되므로 상당히 사소한 경우에만 적용됩니다.


두 번째 경우에 관해서는 컴파일러가 가까운 미래에 그것을 최적화 할 수 있다고 생각하지 않습니다. vec.size ()를 최적화하는 것이 안전하다는 것을 감지하는 것은 벡터 / 손실이 루프 내에서 변경되지 않으면 크기를 증명하는 것에 달려 있습니다.
Lie Ryan

@LieRyan 필자는 결과가 수동으로 "캐시"되고 size ()가 호출 된 경우 컴파일러가 정확히 동일한 이진 파일을 생성 한 여러 사례를 보았습니다. 몇 가지 코드를 작성했으며 프로그램 작동 방식에 따라 동작이 크게 달라집니다. 컴파일러가 루프 중에 벡터 크기가 변경 될 가능성이 없음을 보장 할 수있는 경우가 있으며, 언급 한대로 정지 문제와 매우 유사하게 벡터 크기를 보장 할 수없는 경우가 있습니다. 지금 내가 대답 편집 그래서 내 주장 (C ++ 분해 고통이다)를 확인할 수 없습니다 해요
zxcdw

2
@ 리 라이언 (Lie Ryan) : 일반적인 경우에 결정 할 수없는 많은 것들이 특정하지만 일반적인 경우에 완벽하게 결정할 수 있으며, 실제로 여기에 필요한 전부입니다.
Michael Borgwardt

@LieRyan const이 벡터에서 메소드 만 호출하면 많은 최적화 컴파일러가이를 알아낼 것입니다.
K.Steff

C #에서 Java로 읽은 것으로 생각합니다. 캐시 크기를 캐시하지 않으면 컴파일러는 배열 범위를 벗어나 있는지 확인하기 위해 검사를 제거 할 수 있으며 캐시 크기를 수행하면 검사를 수행해야한다는 것을 알고 있습니다 캐싱을 통해 절약하는 것보다 비용이 많이 듭니다. 옵티 마이저를 능가하는 것은 좋은 계획이 아닙니다.
Kate Gregory

1

사람들은 Java에서 사용할 수있는 트릭 (간단한 컴파일러 플래그 외에도)의 예를 제공 할 수 있습니다.

알고리즘 개선 이외에 메모리 계층 구조 와 프로세서가이를 사용하는 방법 을 고려해야 합니다. 문제의 언어가 데이터 유형과 객체에 메모리를 할당하는 방법을 이해하면 메모리 액세스 대기 시간을 줄이는 데 큰 이점이 있습니다.

1000x1000 정수 배열에 액세스하는 Java 예제

아래 샘플 코드를 고려하십시오-동일한 메모리 영역 (1000x1000 배열의 int)에 액세스하지만 다른 순서로 액세스합니다. 내 Mac mini (Core i7, 2.7GHz)에서 출력은 다음과 같으며, 행으로 배열을 순회 하면 성능 이 두 배 이상 증가 합니다 (각 100 회 이상 평균).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

이는 연속 열 (즉, int 값)이 메모리에 인접하게 배치되는 반면 연속 행은 그렇지 않도록 배열이 저장되기 때문입니다. 프로세서가 실제로 데이터를 사용하려면 데이터를 캐시로 전송해야합니다. 메모리 전송은 캐시 라인 이라고하는 바이트 블록에 의해 이루어집니다. 메모리에서 직접 캐시 라인을로드하면 대기 시간이 발생하여 프로그램 성능이 저하됩니다.

Core i7 (샌디 브릿지)의 경우 캐시 라인은 64 바이트를 보유하므로 각 메모리 액세스는 64 바이트를 검색합니다. 첫 번째 테스트는 예측 가능한 순서로 메모리에 액세스하기 때문에 프로세서는 실제로 프로그램에서 사용하기 전에 데이터를 프리 페치합니다. 전반적으로 메모리 액세스 대기 시간이 줄어들어 성능이 향상됩니다.

샘플 코드 :

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }

1

JVM은 종종 방해 할 수 있으며 JIT 컴파일러는 버전간에 크게 변경 될 수 있습니다. 하이퍼 스레딩에 익숙하거나 최신 Intel 프로세서의 SIMD 모음과 같은 언어 제한으로 인해 Java에서는 일부 미세 최적화가 불가능합니다.

Disruptor 작성자가 작성한 주제에 대한 유익한 블로그를 읽는 것이 좋습니다.

마이크로 최적화를 원하는 경우 Java를 사용하는 이유가 무엇인지 항상 묻어 야합니다 .JNA 또는 JNI를 사용하여 기본 라이브러리로 전달하는 것과 같은 함수의 가속화 방법에는 여러 가지가 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.