일어날 수도 있고 일어나지 않을 수도있는 것에 대해 추측하는 대신에, 우리는 보자고? 나는 (비록 핸디 C # 컴파일러를 가지고 있지 않기 때문에 C ++을 사용해야합니다 은 C # 예제 참조 에서 VisualMelon을 ),하지만 난 같은 원칙에 관계없이 적용 확신합니다.
인터뷰에서 접한 두 가지 대안이 포함됩니다. 또한 abs
일부 답변에서 제안한대로 사용하는 버전도 포함됩니다 .
#include <cstdlib>
bool IsSumInRangeWithVar(int a, int b)
{
int s = a + b;
if (s > 1000 || s < -1000) return false;
else return true;
}
bool IsSumInRangeWithoutVar(int a, int b)
{
if (a + b > 1000 || a + b < -1000) return false;
else return true;
}
bool IsSumInRangeSuperOptimized(int a, int b) {
return (abs(a + b) < 1000);
}
이제 최적화없이 컴파일하십시오. g++ -c -o test.o test.cpp
이제 우리는 이것이 무엇을 생성하는지 정확하게 볼 수 있습니다. objdump -d test.o
0000000000000000 <_Z19IsSumInRangeWithVarii>:
0: 55 push %rbp # begin a call frame
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d ec mov %edi,-0x14(%rbp) # save first argument (a) on stack
7: 89 75 e8 mov %esi,-0x18(%rbp) # save b on stack
a: 8b 55 ec mov -0x14(%rbp),%edx # load a and b into edx
d: 8b 45 e8 mov -0x18(%rbp),%eax # load b into eax
10: 01 d0 add %edx,%eax # add a and b
12: 89 45 fc mov %eax,-0x4(%rbp) # save result as s on stack
15: 81 7d fc e8 03 00 00 cmpl $0x3e8,-0x4(%rbp) # compare s to 1000
1c: 7f 09 jg 27 # jump to 27 if it's greater
1e: 81 7d fc 18 fc ff ff cmpl $0xfffffc18,-0x4(%rbp) # compare s to -1000
25: 7d 07 jge 2e # jump to 2e if it's greater or equal
27: b8 00 00 00 00 mov $0x0,%eax # put 0 (false) in eax, which will be the return value
2c: eb 05 jmp 33 <_Z19IsSumInRangeWithVarii+0x33>
2e: b8 01 00 00 00 mov $0x1,%eax # put 1 (true) in eax
33: 5d pop %rbp
34: c3 retq
0000000000000035 <_Z22IsSumInRangeWithoutVarii>:
35: 55 push %rbp
36: 48 89 e5 mov %rsp,%rbp
39: 89 7d fc mov %edi,-0x4(%rbp)
3c: 89 75 f8 mov %esi,-0x8(%rbp)
3f: 8b 55 fc mov -0x4(%rbp),%edx
42: 8b 45 f8 mov -0x8(%rbp),%eax # same as before
45: 01 d0 add %edx,%eax
# note: unlike other implementation, result is not saved
47: 3d e8 03 00 00 cmp $0x3e8,%eax # compare to 1000
4c: 7f 0f jg 5d <_Z22IsSumInRangeWithoutVarii+0x28>
4e: 8b 55 fc mov -0x4(%rbp),%edx # since s wasn't saved, load a and b from the stack again
51: 8b 45 f8 mov -0x8(%rbp),%eax
54: 01 d0 add %edx,%eax
56: 3d 18 fc ff ff cmp $0xfffffc18,%eax # compare to -1000
5b: 7d 07 jge 64 <_Z22IsSumInRangeWithoutVarii+0x2f>
5d: b8 00 00 00 00 mov $0x0,%eax
62: eb 05 jmp 69 <_Z22IsSumInRangeWithoutVarii+0x34>
64: b8 01 00 00 00 mov $0x1,%eax
69: 5d pop %rbp
6a: c3 retq
000000000000006b <_Z26IsSumInRangeSuperOptimizedii>:
6b: 55 push %rbp
6c: 48 89 e5 mov %rsp,%rbp
6f: 89 7d fc mov %edi,-0x4(%rbp)
72: 89 75 f8 mov %esi,-0x8(%rbp)
75: 8b 55 fc mov -0x4(%rbp),%edx
78: 8b 45 f8 mov -0x8(%rbp),%eax
7b: 01 d0 add %edx,%eax
7d: 3d 18 fc ff ff cmp $0xfffffc18,%eax
82: 7c 16 jl 9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
84: 8b 55 fc mov -0x4(%rbp),%edx
87: 8b 45 f8 mov -0x8(%rbp),%eax
8a: 01 d0 add %edx,%eax
8c: 3d e8 03 00 00 cmp $0x3e8,%eax
91: 7f 07 jg 9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
93: b8 01 00 00 00 mov $0x1,%eax
98: eb 05 jmp 9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
9a: b8 00 00 00 00 mov $0x0,%eax
9f: 5d pop %rbp
a0: c3 retq
스택 에서 16 개의 추가 바이트 를 사용 하는 스택 주소 (예 : -0x4
in mov %edi,-0x4(%rbp)
및 -0x14
in mov %edi,-0x14(%rbp)
)를 볼 수 있습니다 IsSumInRangeWithVar()
.
IsSumInRangeWithoutVar()
중간 값을 저장하기 위해 스택에 공간을 할당하지 않기 때문에 s
다시 계산해야하므로이 구현은 2 개의 명령이 더 길어집니다.
웃기는, -1000과 1000 초를 비교하는 것을 제외하고는 IsSumInRangeSuperOptimized()
많이 비슷 IsSumInRangeWithoutVar()
합니다.
이제 가장 기본적인 최적화만으로 컴파일 해 보자 : g++ -O1 -c -o test.o test.cpp
. 결과:
0000000000000000 <_Z19IsSumInRangeWithVarii>:
0: 8d 84 37 e8 03 00 00 lea 0x3e8(%rdi,%rsi,1),%eax
7: 3d d0 07 00 00 cmp $0x7d0,%eax
c: 0f 96 c0 setbe %al
f: c3 retq
0000000000000010 <_Z22IsSumInRangeWithoutVarii>:
10: 8d 84 37 e8 03 00 00 lea 0x3e8(%rdi,%rsi,1),%eax
17: 3d d0 07 00 00 cmp $0x7d0,%eax
1c: 0f 96 c0 setbe %al
1f: c3 retq
0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>:
20: 8d 84 37 e8 03 00 00 lea 0x3e8(%rdi,%rsi,1),%eax
27: 3d d0 07 00 00 cmp $0x7d0,%eax
2c: 0f 96 c0 setbe %al
2f: c3 retq
그것을 보시겠습니까? 각 변형은 동일합니다 . 컴파일러는 매우 영리한 일을 할 수 있습니다 . 부호없는 비교 abs(a + b) <= 1000
를 a + b + 1000 <= 2000
고려 하는 것과 setbe
같으므로 음수가 매우 큰 양수가됩니다. lea
명령은 실제로 하나 개의 명령어에 모든 추가 사항을 수행하고 모든 조건 분기를 제거 할 수 있습니다.
귀하의 질문에 대답하기 위해 거의 항상 최적화해야 할 것은 메모리 나 속도가 아니라 가독성 입니다. 코드를 읽는 것은 코드를 작성하는 것보다 훨씬 어렵고 "최적화"하기 위해 엉망인 코드를 읽는 것은 명확하게 작성된 코드를 읽는 것보다 훨씬 어렵습니다. 보다 더 자주는 아니지만,이 "최적화"무시할 수, 또는이 경우와 같이 제로 정확히 성능에 실제 영향.
후속 질문,이 코드가 컴파일 된 대신 해석 된 언어로 변경되면 어떤 변화가 있습니까? 그렇다면 최적화가 중요합니까, 아니면 동일한 결과가 있습니까?
측정하자! 예제를 파이썬으로 번역했습니다.
def IsSumInRangeWithVar(a, b):
s = a + b
if s > 1000 or s < -1000:
return False
else:
return True
def IsSumInRangeWithoutVar(a, b):
if a + b > 1000 or a + b < -1000:
return False
else:
return True
def IsSumInRangeSuperOptimized(a, b):
return abs(a + b) <= 1000
from dis import dis
print('IsSumInRangeWithVar')
dis(IsSumInRangeWithVar)
print('\nIsSumInRangeWithoutVar')
dis(IsSumInRangeWithoutVar)
print('\nIsSumInRangeSuperOptimized')
dis(IsSumInRangeSuperOptimized)
print('\nBenchmarking')
import timeit
print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),))
Python 3.5.2로 실행하면 출력이 생성됩니다.
IsSumInRangeWithVar
2 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_ADD
7 STORE_FAST 2 (s)
3 10 LOAD_FAST 2 (s)
13 LOAD_CONST 1 (1000)
16 COMPARE_OP 4 (>)
19 POP_JUMP_IF_TRUE 34
22 LOAD_FAST 2 (s)
25 LOAD_CONST 4 (-1000)
28 COMPARE_OP 0 (<)
31 POP_JUMP_IF_FALSE 38
4 >> 34 LOAD_CONST 2 (False)
37 RETURN_VALUE
6 >> 38 LOAD_CONST 3 (True)
41 RETURN_VALUE
42 LOAD_CONST 0 (None)
45 RETURN_VALUE
IsSumInRangeWithoutVar
9 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_ADD
7 LOAD_CONST 1 (1000)
10 COMPARE_OP 4 (>)
13 POP_JUMP_IF_TRUE 32
16 LOAD_FAST 0 (a)
19 LOAD_FAST 1 (b)
22 BINARY_ADD
23 LOAD_CONST 4 (-1000)
26 COMPARE_OP 0 (<)
29 POP_JUMP_IF_FALSE 36
10 >> 32 LOAD_CONST 2 (False)
35 RETURN_VALUE
12 >> 36 LOAD_CONST 3 (True)
39 RETURN_VALUE
40 LOAD_CONST 0 (None)
43 RETURN_VALUE
IsSumInRangeSuperOptimized
15 0 LOAD_GLOBAL 0 (abs)
3 LOAD_FAST 0 (a)
6 LOAD_FAST 1 (b)
9 BINARY_ADD
10 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
13 LOAD_CONST 1 (1000)
16 COMPARE_OP 1 (<=)
19 RETURN_VALUE
Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s
바이트 코드 "컴파일러"는 최적화에 많은 도움이되지 않기 때문에 파이썬에서 디스 어셈블리는 그리 흥미롭지 않습니다.
세 가지 기능의 성능은 거의 동일합니다. IsSumInRangeWithVar()
한계 속도 게인으로 인해 유혹을받을 수도 있습니다 . 에 다른 매개 변수를 시도하면서 추가 할 것이지만 timeit
때로는 IsSumInRangeSuperOptimized()
가장 빠르기 때문에 구현의 본질적 이점보다는 차이를 담당하는 외부 요인 일 수 있습니다.
이것이 실제로 성능에 중요한 코드 인 경우, 해석 된 언어는 매우 좋지 않은 선택입니다. pypy와 동일한 프로그램을 실행하면 다음과 같은 결과가 나타납니다.
IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s
JIT 컴파일을 사용하여 많은 인터프리터 오버 헤드를 제거하는 pypy를 사용하는 것만으로도 1 또는 2 배의 성능 향상을 얻을 수 있습니다. 나는 IsSumInRangeWithVar()
다른 것보다 훨씬 빠른 속도라는 것을보고 매우 놀랐 습니다. 그래서 벤치 마크 순서를 변경하고 다시 실행했습니다.
IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
따라서 실제로 구현 속도가 빨라지는 것이 아니라 벤치마킹을 수행하는 순서가 아닙니다!
솔직히 나는 왜 이런 일이 발생하는지 알지 못하기 때문에 이것에 대해 더 깊이 파고 싶습니다. 그러나 중간 지점 값을 변수로 선언할지 여부와 같은 미세 최적화는 거의 관련이 없습니다. 해석 언어 또는 고도로 최적화 된 컴파일러를 사용하는 첫 번째 목표는 여전히 명확한 코드를 작성하는 것입니다.
추가 최적화가 필요할 경우 벤치마킹하십시오 . 최고의 최적화는 작은 세부 사항이 아니라 더 큰 알고리즘 그림에서 나온다는 것을 기억하십시오 .pypy는 더 빠른 알고리즘 (JIT 컴파일러 대 해석)을 사용하여 프로그램. 코딩 알고리즘도 고려해야합니다. B- 트리를 통한 검색은 연결된 목록보다 빠릅니다.
당신이 작업에 적합한 도구와 알고리즘을 사용하고 확인한 후 뛰어들 준비가되어 깊은 시스템의 세부 사항에. 결과는 숙련 된 개발자에게도 놀라 울 수 있으므로 변경 사항을 수량화하기위한 벤치 마크가 있어야합니다.