메소드의 메모리 대 성능 속도를 언제 최적화해야합니까?


107

나는 최근 아마존에서 인터뷰했다. 코딩 세션 중에 면접관은 왜 메소드에서 변수를 선언했는지 묻습니다. 나는 나의 과정을 설명했고 그는 더 적은 변수로 같은 문제를 해결하도록 도전했다. 예를 들어 (이것은 인터뷰에서 나온 것이 아닙니다), 나는 방법 A로 시작한 다음 을 제거 하여 방법 B개선 했습니다 . 그는이 방법으로 메모리 사용을 줄일 것이라고 기뻐했습니다.int s

나는 그 논리를 이해하지만 내 질문은 다음과 같습니다.

방법 A와 방법 B를 사용하는 것이 언제 적절한가?

당신이 볼 수있는 방법 A가 있기 때문에, 더 높은 메모리 사용을해야 할 것입니다 int s선언하지만, 그것은 단지, 즉 하나의 계산을 수행 할 수있다 a + b. 반면, 방법 B 는 메모리 사용량이 낮지 만 두 가지 계산, 즉 a + b두 번의 계산을 수행해야 합니다. 한 기술을 다른 기술보다 언제 사용합니까? 아니면 기술 중 하나가 항상 다른 것보다 선호됩니까? 두 가지 방법을 평가할 때 고려해야 할 사항은 무엇입니까?

방법 A :

private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

방법 B :

private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

229
현대 컴파일러가 두 경우 모두에 대해 동일한 어셈블리를 생성 할 것이라고 확신합니다.
17 of 26

12
편집 한 내용이 답변을 무효화했기 때문에 질문을 원래 상태로 롤백했습니다. 그렇게하지 마십시오. 코드를 개선하는 방법에 대해 질문하는 경우 표시된 방식으로 코드를 개선하여 질문을 변경하지 마십시오. 그러면 답변이 의미가 없습니다.
Doc Brown

76
잠깐만, 그들은 int s상한과 하한을위한 마법의 숫자로 완전히 괜찮은 상태에서 제거하라고 요청 했습니까?
null

34
기억하기 : 최적화하기 전에 프로파일하십시오. 최신 컴파일러에서는 방법 A와 방법 B가 동일한 코드로 최적화 될 수 있습니다 (최적화 수준 사용). 또한 최신 프로세서를 사용하면 단일 작업에서 추가 이상의 기능을 수행하는 명령을 가질 수 있습니다.
토마스 매튜

142
둘 다; 가독성을 최적화하십시오.
Andy

답변:


148

일어날 수도 있고 일어나지 않을 수도있는 것에 대해 추측하는 대신에, 우리는 보자고? 나는 (비록 핸디 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 개의 추가 바이트 를 사용 하는 스택 주소 (예 : -0x4in mov %edi,-0x4(%rbp)-0x14in 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) <= 1000a + 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- 트리를 통한 검색은 연결된 목록보다 빠릅니다.

당신이 작업에 적합한 도구와 알고리즘을 사용하고 확인한 후 뛰어들 준비가되어 깊은 시스템의 세부 사항에. 결과는 숙련 된 개발자에게도 놀라 울 수 있으므로 변경 사항을 수량화하기위한 벤치 마크가 있어야합니다.


6
C #에서 예제 제공 : SharpLab은 두 방법 모두에 대해 동일한 asm을 생성합니다 (x86의 Desktop CLR v4.7.3130.00 (clr.dll))
VisualMelon

2
@VisualMelon funilly 긍정적 인 확인 : "return (((a + b)> = -1000) && ((a + b) <= 1000));"은 다른 결과를 제공합니다. : sharplab.io/…
Pieter B

12
가독성으로 인해 프로그램을보다 쉽게 ​​최적화 할 수 있습니다. 컴파일러는 실제로 수행하려는 작업을 실제로 파악할 수있는 경우에만 위와 동일한 논리 를 사용하도록 쉽게 다시 작성할 수 있습니다 . 많은 구식 비트 해킹 을 사용하고 int와 포인터간에 캐스트하고 변경 가능한 스토리지 등을 재사용하는 경우 컴파일러가 변환이 동등하다는 것을 증명하기가 훨씬 어려울 수 있습니다. 이 방법은 차선책 일 수 있습니다.
Leushenko

1
@Corey는 편집을 참조하십시오.
Phil Frost

2
@Corey :이 답변은 실제로 내가 대답 한 내용을 정확하게 알려줍니다. 괜찮은 컴파일러를 사용할 때 차이가 없으며 대신 준비에 초점을 맞 춥니 다. 물론, 그것은 더 잘 설립 된 것처럼 보입니다. 아마도 당신은 지금 나를 믿습니다.
Doc Brown

67

명시된 질문에 대답하려면 :

메소드의 메모리 대 성능 속도를 언제 최적화해야합니까?

설정해야 할 두 가지가 있습니다.

  • 응용 프로그램을 제한하는 것은 무엇입니까?
  • 해당 리소스를 최대한 활용하려면 어디서해야합니까?

첫 번째 질문에 답하려면 응용 프로그램의 성능 요구 사항이 무엇인지 알아야합니다. 성능 요구 사항이없는 경우 한 가지 방법으로 최적화 할 이유가 없습니다. 성능 요구 사항은 "충분히 좋은"장소에 도달하는 데 도움이됩니다.

자체적으로 제공 한 방법은 자체적으로 성능 문제를 일으키지 않지만 루프 내에서 많은 양의 데이터를 처리하는 경우 문제에 어떻게 접근하는지에 대해 조금 다르게 생각해야합니다.

응용 프로그램을 제한하는 내용 감지

성능 모니터를 사용하여 응용 프로그램의 동작을 살펴보십시오. 실행중인 CPU, 디스크, 네트워크 및 메모리 사용량을 주시하십시오. 완벽한 균형을 맞추지 않는 한 거의 일어나지 않는 한, 하나 이상의 항목이 최대로 사용되며 다른 모든 항목은 적당히 사용됩니다.

더 깊게 보일 필요가있을 때 일반적으로 프로파일 러를 사용합니다 . 가 있습니다 메모리 프로파일 러프로세스 프로파일 러 , 그들은 다른 것을 측정합니다. 프로파일 링 작업은 성능에 큰 영향을 미치지 만 코드를 계측하여 무엇이 잘못되었는지 확인합니다.

CPU와 디스크 사용량이 최고라고 가정 해 봅시다. 먼저 "핫스팟"또는 나머지보다 더 자주 호출되거나 처리 시간이 상당히 긴 코드를 확인합니다.

핫스팟을 찾을 수 없으면 메모리를 살펴보기 시작합니다. 아마도 필요한 것보다 많은 객체를 만들고 가비지 수집이 초과 작업 중일 수 있습니다.

성능 재 확보

비판적으로 생각하십시오. 다음의 변경 목록은 얻을 수있는 투자 수익의 순서입니다.

  • 아키텍처 : 커뮤니케이션 초크 포인트 찾기
  • 알고리즘 : 데이터 처리 방식을 변경해야 할 수도 있습니다.
  • 핫스팟 : 핫스팟을 호출하는 빈도를 최소화하면 큰 보너스를 얻을 수 있습니다
  • 마이크로 최적화 : 일반적이지 않지만 때로는 코드에서 핫 스팟 인 경우 약간의 수정 (예 : 제공 한 예)을 고려해야합니다.

이와 같은 상황에서는 과학적 방법을 적용해야합니다. 가설을 세우고 변경하고 테스트하십시오. 성과 목표를 달성하면 완료된 것입니다. 그렇지 않은 경우 목록에서 다음으로 이동하십시오.


굵게 표시된 질문에 답변 :

방법 A와 방법 B를 사용하는 것이 언제 적절한가?

솔직히 이것은 성능 또는 메모리 문제를 해결하기위한 마지막 단계입니다. 방법 A와 방법 B의 영향은 언어 플랫폼 (일부 경우)에 따라 실제로 다릅니다 .

중간 정도의 최적화 프로그램이 있는 컴파일 된 언어 는 이러한 구조 중 하나를 사용하여 유사한 코드를 생성합니다. 그러나 이러한 가정이 옵티마이 저가없는 독점 및 완구 언어에서 반드시 적용되는 것은 아닙니다.

더 나은 영향을 미치는 정확한 sum것은 스택 변수인지 힙 변수 인지에 달려 있습니다. 이것은 언어 구현 선택입니다. 예를 들어 C, C ++ 및 Java에서 a와 같은 숫자 기본 요소 int는 기본적으로 스택 변수입니다. 코드는 완전히 인라인 된 코드보다 스택 변수에 할당하여 더 이상 메모리에 영향을 미치지 않습니다.

C 라이브러리 (특히 오래된 것)에서 찾을 수있는 다른 최적화는 2 차원 배열을 먼저 복사하거나 아래로 복사하는 것을 결정할 수있는 플랫폼에 따라 다릅니다. 대상 칩셋이 메모리 액세스를 가장 최적화하는 방법에 대한 지식이 필요합니다. 아키텍처에는 미묘한 차이가 있습니다.

결론은 최적화가 예술과 과학의 조합이라는 것입니다. 문제에 접근하는 방법에 대한 융통성뿐만 아니라 비판적 사고가 필요합니다. 작은 것을 비난하기 전에 큰 것을 찾으십시오.


2
이 답변은 내 질문에 가장 초점을 맞추고 있으며 코딩 예제, 즉 방법 A와 방법 B에 따라 잡히지 않습니다.
Corey P

18
나는 이것이 "성능 병목 현상을 어떻게 해결합니까?"에 대한 일반적인 대답이라고 생각하지만이 방법을 사용하여 4 또는 5 개의 변수가 있는지 여부에 따라 특정 기능에서 상대 메모리 사용량을 식별하기가 어려울 것입니다. 또한 컴파일러 (또는 인터프리터)가 이것을 최적화하거나 최적화하지 않을 때이 최적화 수준이 얼마나 관련성이 있는지 질문합니다.
Eric

@Eric, 내가 언급했듯이 성능 개선의 마지막 범주는 마이크로 최적화입니다. 영향을 줄 수있는 유일한 방법은 프로파일 러에서 성능 / 메모리를 측정하는 것입니다. 이러한 유형의 개선이 성과를 거두는 경우는 드물지만, 타이밍에 민감한 성능 문제에서 시뮬레이터에있는 몇 가지 변경 사항은 타이밍 목표를 맞추는 것과의 차이가 될 수 있습니다. 저는 20 년 넘게 소프트웨어 작업을해온 결과를 한 번에 셀 수 있지만 제로가 아닙니다.
Berin Loritsch

@BerinLoritsch 다시 한번, 일반적으로 나는 당신에 동의하지만,이 특정한 경우에는 그렇지 않습니다. 나는 내 자신의 답변을 제공했지만 함수의 스택 메모리 크기와 관련된 성능 문제를 잠재적으로 식별하는 방법을 알려주거나 표시하는 도구를 개인적으로 보지 못했습니다.
에릭

@DocBrown, 나는 그것을 고쳤다. 두 번째 질문에 관해서는, 나는 당신에게 거의 동의합니다.
Berin Loritsch

45

"이것은 메모리를 줄일 것이다"-em, no. 이것이 사실 일지라도 (어떤 괜찮은 컴파일러에 대해서는 그렇지 않다), 실제 상황에서는 그 차이를 무시할 수있을 것이다.

그러나 방법 A * (방법 A가 약간 변경됨)를 사용하는 것이 좋습니다.

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}

그러나 완전히 다른 두 가지 이유가 있습니다.

  • 변수 s에 설명 이름 을 부여하면 코드가 더 명확 해집니다.

  • 코드에서 동일한 합산 논리를 두 번 가지지 않으므로 코드가 더 건조 해 지므로 오류가 발생하기 쉽습니다.


36
더 정리하고 "return sum> -1000 && sum <1000;"으로 이동합니다.
17 of 26

36
@Corey 괜찮은 최적화 프로그램은 sum변수에 CPU 레지스터를 사용 하므로 메모리 사용량이 0이됩니다. 그리고 그렇지 않더라도 이것은 "리프 (leaf)"방법에서 단지 하나의 기억의 단어 일뿐입니다. GC 및 객체 모델로 인해 메모리를 낭비하는 Java 또는 C #이 다른 방법으로 사용될 수 있다는 점을 고려하면 로컬 int변수는 문자 그대로 눈에 띄는 메모리를 사용하지 않습니다. 이것은 무의미한 미세 최적화입니다.
amon

10
@Corey : " 조금 더 복잡한"경우 "눈에 띄는 메모리 사용량"이되지 않을 것입니다. 아마도 좀 더 복잡한 예제를 만들면 다른 질문이 될 수 있습니다. 또한 식의 특정 변수를 만들지 않기 때문에 복잡한 중간 결과의 경우 런타임 환경에서 내부적으로 임시 개체를 계속 만들 수 있으므로 언어, 환경, 최적화 수준 및 "눈에 띄는"이라고 부르는 것
Doc Brown

8
위의 사항 외에, 나는 C 번호는 / 자바 저장하기 위해 선택하는 방법을 확신 sum구현 세부 나는 사람이 지역을 피처럼 여부 바보 같은 트릭에 관한 설득력있는 사례를 만들 수 의심 int이 이어질 것 또는 장기적으로 그 메모리 사용량. IMO 가독성이 더 중요합니다. 가독성은 주관적 일 수 있지만 FWIW는 개인적으로 CPU 사용량이 아닌 동일한 계산을 두 번하지 않는 것이지만 버그를 찾을 때 한 번만 추가 사항을 검사해야하기 때문입니다.
jrh

2
일반적으로는 (어쨌든 C #을위한)에만 정리 될 수 예측할 수는 "메모리의 바다를 휘젓는"그가에 ... 또한 쓰레기 수집 된 언어를주의 필요로 할 때 , 나는 RAM의 기가 바이트를 할당하는 프로그램을 기억하고 그것은 단지 시작 " 기억이 부족해 졌을 때 그 자체로 청소. GC를 실행할 필요가 없으면 달콤한 시간이 걸리고 CPU를 절약하여 더 중요한 문제를 해결할 수 있습니다.
jrh

35

당신은 둘 다보다 더 잘 할 수 있습니다

return (abs(a + b) > 1000);

대부분의 프로세서 (및 컴파일러)는 단일 작업으로 abs ()를 수행 할 수 있습니다. 합계가 적을뿐만 아니라 비교가 적어 일반적으로 계산 비용이 많이 듭니다. 또한 분기를 제거하므로 파이프 라이닝이 중지되므로 대부분의 프로세서에서 훨씬 더 나쁩니다.

면접관은 다른 답변에서 알 수 있듯이 식물 생활이며 기술 면담을 수행하는 사업이 없습니다.

그의 질문은 유효합니다. 최적화 할 때와 방법에 대한 답은 필요할 때, 필요한 부분을 정확히 증명할 수 있도록 프로파일 링 한 것 입니다. 크 누스는 유명하지 않은 섹션을 금판으로 만들거나 효과가없는 변경 사항 (면접관처럼)을 변경하는 것이 너무 쉽지 않기 때문에 조기 최적화가 모든 악의 근원이라고 말했다. 확실한 증거를 얻을 때까지 실제로 필요한 것은 코드의 명확성입니다.

편집 FabioTurati는 이것이 원래의 논리와 반대되는 논리라고 생각합니다. (실수입니다!) 이것은 코드를 최적화하려고 할 때 코드를 깨뜨릴 위험이있는 Knuth의 인용문에서 더 큰 영향을 나타냅니다.


2
@Corey, Graham은 요청에 따라 "변수가 적은 동일한 문제를 해결하도록 요청했습니다. " 라는 요청을 고정시킵니다 . 면접관이된다면, 그 대답은 두 번 a+b들어 가지 않고 대답하지 않을 if것입니다. "그는 기쁘고이 방법으로 메모리 사용을 줄일 것이라고 말했다" 고 잘못 이해했습니다. 그는 메모리 에 대한 의미없는 설명으로 실망을 숨기고 기뻤 습니다. 여기서 질문하는 것을 진지하게 생각해서는 안됩니다. 직업을 얻었습니까? 내 생각 엔 :-(
Sinatr

1
동시에 2 개의 변환을 적용하고 있습니다.를 사용하여 2 개의 조건을 1로 바꾸었고 조건이 true 일 때 ( "분기") 다른 조건을 가지지 않고 abs()1 return을 가지는 대신 단일 조건을 갖습니다 ( "다른 지점"). 이와 같이 코드를 변경하면주의해야합니다. 실수로 false를 반환해야 할 때 true를 반환하는 함수를 실수로 작성할 위험이 있으며 그 반대도 마찬가지입니다. 정확히 여기서 일어난 일입니다. 나는 당신이 다른 것에 집중하고 있다는 것을 알고 있으며, 당신은 그것에 좋은 일을했습니다. 아직도, 이것은 당신에게 쉽게 비용이들 수 있습니다 ...
Fabio Turati

2
@ FabioTurati 잘 발견-감사합니다! 답변을 업데이트하겠습니다. 그리고 그것은 리팩토링과 최적화에 대한 좋은 지적이며, Knuth의 인용은 더욱 관련성이 있습니다. 위험을 감수하기 전에 최적화가 필요하다는 것을 증명해야합니다.
Graham

2
대부분의 프로세서 (따라서 컴파일러)는 단일 작업으로 abs ()를 수행 할 수 있습니다. 불행히도 정수의 경우는 아닙니다. ARM64은 플래그가 이미에서 설정되어있는 경우 사용할 수있는 조건 부정이있다 adds, 그리고 ARM 역 서브를 전제로하고있다 ( rsblt= 역 서브 덜 그쪽 경우)하지만 다른 모든 구현하기 위해 여러 추가 명령어가 필요 abs(a+b)하거나 abs(a). godbolt.org/z/Ok_Con 은 x86, ARM, AArch64, PowerPC, MIPS 및 RISC-V asm 출력을 보여줍니다. gcc가 Phil의 답변 에서처럼 최적화 할 수 있는 범위 검사로 비교를 변환하는 것만 (unsigned)(a+b+999) <= 1998U입니다.
Peter Cordes

2
이 답변의 "개선 된"코드는에 대한 다른 답변을 생성하므로 여전히 잘못되었습니다 IsSumInRange(INT_MIN, 0). 원래 코드를 반환 false하기 때문에 INT_MIN+0 > 1000 || INT_MIN+0 < -1000; 하지만 "새롭고 개선 된"코드를 반환 true하기 때문에 abs(INT_MIN+0) < 1000. (또는 일부 언어의 경우 예외가 발생하거나 정의되지 않은 동작이 발생합니다. 지역 목록을 확인하십시오.)
Quuxplusone

16

방법 A와 방법 B를 사용하는 것이 언제 적절한가?

하드웨어는 싸다. 프로그래머는 비싸다 . 따라서이 질문에 두 사람이 낭비하는 시간은 아마도 어느 대답보다 훨씬 나쁩니다.

그럼에도 불구하고, 대부분의 최신 컴파일러는 스택 공간을 할당하는 대신 로컬 변수를 레지스터로 최적화하는 방법을 찾을 수 있으므로 실행 코드 측면에서 메소드가 동일 할 수 있습니다. 이러한 이유로 대부분의 개발자는 의도를 가장 명확하게 전달하는 옵션을 선택합니다 (ROC (실제 코드 작성) 참조 ). 제 생각에는 방법 A가 될 것입니다.

다른 한편으로, 이것이 순수한 학문적 인 경우 방법 C를 사용하여 두 세계를 모두 활용할 수 있습니다.

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}

17
a+=b깔끔한 트릭이지만 매개 변수로 엉망이되는 경험 방법에서 디버깅 및 유지 관리가 매우 어려울 수 있습니다 (나머지 답변에서 암시되지 않는 경우).
jrh

1
@jrh에 동의합니다. 나는 ROC를 강력히지지하고 있으며, 그런 것은 아무것도 아닙니다.
John Wu

3
"하드웨어는 저렴하다. 프로그래머는 비싸다." 가전 ​​제품의 세계에서 그 진술은 거짓입니다. 수백만 대의 장비를 판매하는 경우 추가 개발 비용으로 5 만 달러를 투자하여 단위당 하드웨어 비용을 0.10 달러 절약하는 것이 좋습니다.
Bart van Ingen Schenau

2
@ JohnWu : if검사를 단순화 했지만 비교 결과를 뒤집는 것을 잊었습니다. 범위 내에 있지 않으면 함수가 반환 true됩니다 . 조건을 외부에 추가 하거나 ( ) 반전 테스트를 배포 하여 범위 검사 흐름을 좋게 만들거나a + b!return !(a > 1000 || a < -1000)!return a <= 1000 && a >= -1000;return -1000 <= a && a <= 1000;
ShadowRanger

1
@JohnWu : 아직도 약간 떨어져 에지의 경우에서, 분산에 필요한 논리 <=/가 >=아닌 </ >(로 </ >1000 및 -1000가 범위를 벗어난 것으로 간주되어, 원래의 코드 범위로 처리).
ShadowRanger

11

가독성을 최적화합니다. 방법 X :

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}

한 가지 일을하지만 추론하기 쉬운 작은 방법.

(이것은 개인적인 취향이며, 부정적인 것이 아니라 긍정적 인 테스트를 좋아합니다. 원래 코드는 실제로 값이 범위를 벗어 났는지 여부를 테스트하고 있습니다.)


5
이. (위의 의견은 가독성과 비슷합니다). 30 년 전, 우리는 1MB 미만의 RAM을 가진 기계로 작업 할 때, 성능 저하가 필요했습니다. y2k 문제와 마찬가지로, 사용되지 않은 변수로 인해 몇 바이트의 메모리가 낭비되는 수십만 레코드 256k의 RAM 만 있으면 신속하게 추가됩니다. 이제 우리는 수 기가 바이트의 RAM을 가진 기계를 다루기 때문에 코드의 가독성 및 유지 관리성에 비해 몇 MB의 RAM 사용을 절약하는 것은 좋은 거래가 아닙니다.
Ivanivan

@ivanivan : "y2k 문제"가 실제로 메모리에 관한 것이라고 생각하지 않습니다. 데이터 입력 관점에서 4 자리를 입력하는 것보다 2 자리 숫자를 입력하는 것이 더 효율적이며 입력 한 것을 다른 형식으로 변환하는 것보다 쉽습니다.
supercat

10
이제 무슨 일이 일어나고 있는지 확인하려면 두 가지 기능을 추적해야합니다. 이름에서 포함 또는 독점 범위인지 여부를 알 수 없으므로 액면가로 계산할 수 없습니다. 그리고 해당 정보를 추가하면 함수 이름이 코드를 표현하는 것보다 깁니다.
피터

1
가독성을 최적화하고 추론하기 쉬운 작고 기능을 만드십시오. 하지만 난 강력하게 그 이름을 변경 동의 abnumber1number2어떤 방식으로 에이즈 가독성을. 또한 함수의 이름이 일치하지 않습니다. IsSumInRange범위 IsValueInRange를 인수로 수락 하면 범위를 하드 코딩하는 이유는 무엇입니까?
leftaroundabout

첫 번째 기능이 오버플로 될 수 있습니다. (다른 답변의 코드와 마찬가지로) 오버플로 안전 코드의 복잡성은 코드를 함수에 넣는 인수입니다.
philipxy

6

요컨대, 질문이 현재 컴퓨팅과 관련성이 있다고 생각하지는 않지만 역사적 관점에서 볼 때 흥미로운 생각 운동입니다.

당신의 면접관은 아마도 신화의 달의 팬일 것입니다. 이 책에서 Fred Brooks는 프로그래머가 일반적으로 도구 상자에 메모리 최적화 버전과 CPU 최적화 버전의 두 가지 주요 기능을 필요로한다고 주장합니다. Fred는 기계에 8 킬로바이트 정도의 RAM이있는 IBM System / 360 운영 체제 개발을 주도한 경험을 바탕으로했습니다. 이러한 머신에서 함수에서 로컬 변수에 필요한 메모리는 잠재적으로 중요 할 수 있습니다. 특히 컴파일러가 효과적으로 최적화하지 못한 경우 (또는 코드가 어셈블리 언어로 직접 작성된 경우).

현재 시대에는 메소드에서 로컬 변수의 존재 또는 부재가 눈에 띄는 차이를 만드는 시스템을 찾기가 어려울 것이라고 생각합니다. 변수가 중요하기 위해서는 깊은 재귀가 예상되는 방식으로 재귀를 사용해야합니다. 그럼에도 불구하고 변수 자체에 문제가 발생하기 전에 스택 깊이가 초과되어 스택 오버플로 예외가 발생했을 수 있습니다. 문제가 될 수있는 유일한 실제 시나리오는 재귀 방법으로 스택에 할당 된 매우 큰 배열입니다. 그러나 대부분의 개발자가 불필요한 대형 배열의 사본에 대해 두 번 생각할 것 같지는 않습니다.


4

과제 후 s = a + b; 변수 a와 b는 더 이상 사용되지 않습니다. 따라서 완전히 두뇌 손상 컴파일러를 사용하지 않으면 s에 메모리가 사용되지 않습니다. 어쨌든 a와 b에 사용 된 메모리가 재사용됩니다.

그러나이 기능을 최적화하는 것은 전혀 말이되지 않습니다. 공간을 절약 할 수 있다면 함수가 실행되는 동안 8 바이트 일 수 있습니다 (함수가 반환되면 복구됩니다). 시간을 절약 할 수 있다면 단일 나노초가 될 것입니다. 이것을 최적화하는 것은 총 시간 낭비입니다.


3

로컬 값 유형 변수는 스택에 할당되거나 (이러한 작은 코드 조각의 경우) 프로세서의 레지스터를 사용하며 RAM을 보지 못합니다. 어느 쪽이든 그들은 오래 살았으며 걱정할 것이 없습니다. 잠재적으로 크거나 오래 지속되는 콜렉션에서 데이터 요소를 버퍼링하거나 큐에 넣어야 할 때 메모리 사용을 고려하기 시작합니다.

그런 다음 응용 프로그램에서 가장 중요하게 생각하는 사항에 따라 다릅니다. 처리 속도? 응답 시간? 메모리 풋 프린트? 유지 보수성? 디자인의 일관성? 모두 너에게 달렸다.


4
Nitpicking : 적어도 .NET (포스트의 언어는 지정되지 않음)은 로컬 스택이 "스택"에 할당되는 것을 보장하지 않습니다. Eric Lippert의 "스택은 구현 세부 사항" 을 참조하십시오 .
jrh

1
@jrh 스택 또는 힙의 로컬 변수는 구현 세부 사항 일 수 있지만 누군가가 스택의 변수를 실제로 원한다면 stackalloc지금 그리고 있습니다 Span<T>. 프로파일 링 후 핫스팟에서 유용 할 수 있습니다. 또한 구조체 주위의 일부 문서는 값 형식 스택에 있을 수 있지만 참조 형식 스택에 없을 수 있음을 나타냅니다. 어쨌든 기껏해야 약간의 GC를 피할 수 있습니다.
Bob

2

다른 답변에서 알 수 있듯이 최적화하려는 것을 생각해야합니다.

이 예제에서는 괜찮은 컴파일러가 두 메소드 모두에 대해 동등한 코드를 생성한다고 생각하므로 런타임 이나 메모리 에 영향을 미치지 않습니다 .

그것은 무엇 않은 영향을 미치는 것은 코드의 가독성입니다. (코드는 컴퓨터뿐만 아니라 인간이 읽을 수있는 코드입니다.) 두 예제 사이에는 큰 차이가 없습니다. 다른 모든 것이 평등 할 때는 간결함을 미덕이라고 생각하므로 아마도 방법 B를 선택할 것입니다. 그러나 다른 모든 것들은 거의 같지 않으며 더 복잡한 실제 경우에는 큰 영향을 줄 수 있습니다.

고려해야 할 사항 :

  • 중간 표현에 부작용이 있습니까? 불순한 함수를 호출하거나 변수를 업데이트하는 경우 물론 복제는 스타일뿐만 아니라 정확성의 문제입니다.
  • 중간 표현은 얼마나 복잡합니까? 많은 계산을 수행하거나 함수를 호출하면 컴파일러에서 최적화하지 못할 수 있으므로 성능에 영향을 미칩니다. Knuth가 말했듯 이“우리는 시간의 97 % 정도라는 작은 효율성을 잊어야합니다.”
  • 중간 변수에 의미가 있습니까? 무슨 일이 일어나고 있는지 설명하는 데 도움이되는 이름이 주어질 수 있습니까? 짧지 만 유익한 이름은 코드를 더 잘 설명 할 수 있지만 의미없는 이름은 시각적 소음 일뿐입니다.
  • 중간 표현은 얼마나 걸립니까? 길면 복제하면 코드를 더 길고 읽기 어려울 수 있습니다 (특히 줄 바꿈을 강요하는 경우). 그렇지 않으면 복제가 전체보다 짧을 수 있습니다.

1

많은 답변이 지적했듯이 현대 컴파일러 로이 기능을 조정하려고해도 아무런 차이가 없습니다. 옵티마이 저는 아마도 최상의 솔루션을 알아낼 수 있습니다 (어셈블러 코드를 보여준 대답에 찬성 투표하십시오!). 인터뷰의 코드는 정확히 비교하도록 요청한 코드가 아니므로 실제 예제가 좀 더 의미가 있습니다.

그러나이 질문을 다시 한번 살펴 봅시다 : 이것은 인터뷰 질문입니다. 진짜 문제는, 당신이 직업을 구하려고한다고 가정 할 때 어떻게 대답해야합니까?

또한 면접관이 자신이 말하는 내용을 알고 있고 자신이 알고있는 내용 만 보려고한다고 가정 해 봅시다.

최적화 프로그램을 무시하고 첫 번째는 스택에 임시 변수를 만들 수 있지만 두 번째는 그렇지 않지만 계산을 두 번 수행한다고 언급합니다. 따라서 첫 번째는 더 많은 메모리를 사용하지만 더 빠릅니다.

어쨌든 계산에는 결과를 저장하기 위해 임시 변수가 필요할 수 있으므로 (비교하기 위해) 변수의 이름을 지정했는지 여부에 관계없이 계산할 수 있습니다.

그런 다음 실제로 모든 변수가 로컬이기 때문에 코드가 최적화되고 대부분의 기계 코드가 생성 될 것이라고 언급합니다. 그러나 사용중인 컴파일러에 따라 다릅니다 (오래 전에 로컬 변수를 Java에서 "최종"으로 선언하여 유용한 성능 향상을 얻을 수는 없었습니다).

어떤 경우에도 스택은 자체 메모리 페이지에 존재하므로 추가 변수로 인해 스택이 페이지를 오버플로하지 않는 한 실제로는 더 이상 메모리를 할당하지 않습니다. 오버플로가 발생하면 완전히 새로운 페이지가 필요합니다.

더 현실적인 예는 캐시를 사용하여 많은 계산 결과를 보유할지 여부를 선택하는 것일 수 있으며 이는 CPU 대 메모리 문제를 제기합니다.

이 모든 것은 당신이 무슨 말을하는지 알고 있음을 보여줍니다.

나는 대신에 readabilty에 집중하는 것이 낫다고 말하기 위해 그것을 끝낼 것입니다. 이 경우에는 사실이지만 인터뷰 상황에서는 "성능에 대해서는 모르겠지만 코드는 Janet와 John 이야기 처럼 읽습니다"로 해석 될 수 있습니다 .

하지 말아야 할 것은 코드 최적화가 필요하지 않은 방법에 대한 일반적인 성명을 트로트하는 것입니다. 코드를 프로파일 링 할 때까지 최적화하지 마십시오 (이것은 사용자가 나쁜 코드를 볼 수 없음을 나타냅니다), 하드웨어 비용은 프로그래머보다 적습니다 , 제발, 제발, Knuth를 "조기의 blah blah ..."라고 인용하지 마십시오.

코드 성능은 많은 조직에서 진정한 문제이며 많은 조직에서는이를 이해하는 프로그래머가 필요합니다.

특히 Amazon과 같은 조직에서는 일부 코드에 큰 영향이 있습니다. 코드 스 니펫은 수천 대의 서버 또는 수백만 개의 장치에 배포 될 수 있으며 매일 매일 수십억 번 호출 될 수 있습니다. 수천 개의 유사한 스 니펫이있을 수 있습니다. 잘못된 알고리즘과 좋은 알고리즘의 차이는 쉽게 수천 배가 될 수 있습니다. 숫자와이 모든 것을 여러 번하십시오 : 차이가 있습니다. 시스템 성능이 떨어지면 성능이 좋지 않은 코드를 구성하는 데 드는 잠재적 비용은 매우 중요하거나 치명적일 수 있습니다.

더욱이 이들 조직 중 다수는 경쟁력있는 환경에서 일합니다. 따라서 경쟁 업체의 소프트웨어가 이미 보유하고있는 하드웨어에서 제대로 작동하거나 소프트웨어가 모바일 핸드셋에서 실행되고 업그레이드 할 수없는 경우 고객에게 더 큰 컴퓨터를 사라고 지시 할 수는 없습니다. 일부 응용 프로그램은 특히 성능이 중요하고 (게임 및 모바일 응용 프로그램을 염두에두고) 응답 성 또는 속도에 따라 작동하거나 죽을 수 있습니다.

개인적으로 20 년이 넘게 성능 문제로 인해 시스템이 고장 나거나 사용할 수없는 많은 프로젝트를 수행해 왔으며 시스템 최적화에 부름을 받았으며 모든 경우에 이해하지 못하는 프로그래머가 작성한 잘못된 코드로 인해 발생했습니다 그들이 쓰고있는 것의 영향. Furthmore, 그것은 결코 한 조각의 코드가 아니며 항상 어디에나 있습니다. 내가 일어 났을 때, 성능에 대해 생각하기 시작하는 것은 늦었다. 피해는 끝났다.

코드 성능을 이해하는 것은 코드 정확성과 코드 스타일을 이해하는 것과 같은 방법으로 좋은 기술입니다. 연습에서 나옵니다. 성능 장애는 기능 장애만큼이나 나쁠 수 있습니다. 시스템이 작동하지 않으면 작동하지 않습니다. 이유는 중요하지 않습니다. 마찬가지로, 사용되지 않은 성능과 기능은 모두 나쁩니다.

따라서 면접관이 성능에 대해 물으면 가능한 한 많은 지식을 시도하고 시연하는 것이 좋습니다. 질문이 나쁜 것 같으면 그 경우에 문제가되지 않을 것이라고 생각하는 이유를 정중하게 지적하십시오. 크 누스를 인용하지 마십시오.


0

먼저 정확성을 최적화해야합니다.

Int.MaxValue에 가까운 입력 값에 대해 함수가 실패합니다.

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

합계가 -400으로 오버플로되므로 true를 반환합니다. 이 함수는 a = int.MinValue + 200에서도 작동하지 않습니다 (잘못된 "400"까지 추가)

우리는 면접관이 어떤 소리를 내지 않는 한 무엇을 찾고 있었는지 알지 못하지만 "오버플로가 현실적" 입니다.

인터뷰 상황에서 문제의 범위를 명확히하기 위해 질문하십시오 : 허용되는 최대 및 최소 입력 값은 무엇입니까? 그런 다음 호출자가 범위 밖의 값을 제출하면 예외가 발생할 수 있습니다. 또는 (C #에서) 검사 된 {} 섹션을 사용하면 오버플로시 예외가 발생합니다. 그렇습니다. 더 효과적이고 복잡하지만 때로는 그것이 필요합니다.


방법은 단지 예일뿐입니다. 그것들은 옳은 것이 아니라 실제 질문을 설명하기 위해 쓰여졌습니다. 그래도 입력 주셔서 감사합니다!
Corey P

인터뷰 질문은 성과에 관한 것이므로 질문의 의도에 답해야합니다. 면접관은 한도에서의 행동에 대해 묻지 않습니다. 그러나 어쨌든 흥미로운 측면.
rghome

1
@Corey Good 면접관은 1) rghome이 제안한 것처럼 문제에 대한 후보 능력을 평가합니다 .2) 큰 문제 (무언의 기능 정확성과 같은)와 관련 지식의 깊이에 대한 개방으로 간주합니다. 나중에 경력 인터뷰에서-행운을 빕니다.
chux

0

귀하의 질문은 "이것을 전혀 최적화해야합니까?"였습니다.

버전 A와 B는 A를 선호하게 만드는 한 가지 중요한 세부 사항이 다르지만 최적화와 관련이 없습니다. 코드를 반복하지 않습니다.

실제 "최적화"를 공통 하위 식 제거라고하며, 이는 거의 모든 컴파일러가하는 일입니다. 일부는 최적화가 해제 된 경우에도이 기본 최적화를 수행합니다. 따라서 이것이 진정한 최적화는 아닙니다 (생성 된 코드는 모든 경우에 정확히 동일 할 것입니다).

그러나 그것이 최적화 가 아니라면 왜 선호 하는가? 좋아, 당신은 걱정하는 코드를 반복하지 않습니다!

우선, 우연히 조건절의 절반이 잘못 될 위험이 없습니다. 그러나 더 중요한 것은이 코드를 읽는 사람이 경험 대신 즉시 수행하려는 작업을 수행 할 수 있다는 if((((wtf||is||this||longexpression))))것입니다. 독자가 보게 if(one || theother)되는 것은 좋은 것입니다. 아니 거의, 그 변화가 없습니다 당신이 다른 사람이 3 년 후에 자신의 코드를 읽고 생각하는 것이있다 "WTF는이 뜻인가?". 이 경우 코드가 의도를 즉시 전달하면 항상 도움이됩니다. 공통 하위 표현식의 이름이 올바르게 지정되면이 경우입니다.
미래의 어느 시점에, 당신은 당신이 필요로 예를 들어 변경하는 것을 결정하는 경우 또한, a+ba-b, 당신은 변경해야 하나둘이 아니라 위치. 그리고 실수로 두 번째 잘못을 당할 위험이 없습니다.

실제 질문에 대해 무엇을 최적화해야합니까? 먼저 모든 코드가 정확 해야 합니다 . 이것은 절대적으로 가장 중요한 것입니다. 정확하지 않은 코드는 잘못된 코드입니다. 심지어 잘못 되었더라도 "정상적으로 작동"하거나 최소한 제대로 작동 하는 것처럼 보입니다 . 그 후에는 코드를 읽을 수 있어야합니다 (잘 모르는 사람이 읽을 수 있음).
최적화에 관해서는 ... 최적화되지 않은 코드를 의도적으로 작성해서는 안되며, 시작하기 전에 디자인에 대해 생각하지 않아야합니다 (예 : 문제에 대한 올바른 알고리즘 선택, 가장 효율적인 것은 아닙니다).

그러나 대부분의 응용 프로그램, 대부분의 경우 최적화 컴파일러를 통해 합리적인 알고리즘을 사용하여 읽을 수있는 올바른 코드를 실행 한 후에 얻는 성능은 괜찮습니다. 실제로 걱정할 필요가 없습니다.

그렇지 않은 경우, 즉 응용 프로그램의 성능이 실제로 요구 사항을 충족하지 못하는 경우 에만 시도한 것과 같은 로컬 최적화를 수행하는 것에 대해 걱정해야합니다. 그러나 최상위 알고리즘을 다시 고려하는 것이 좋습니다. 더 나은 알고리즘으로 인해 50,000 회 대신 500 회 함수를 호출하면 미세 최적화에 3 개의 클록주기를 저장하는 것보다 더 큰 영향을 미칩니다. 당신이 경우 하지 않는 임의의 메모리 액세스에 수백주기 위해 모든 시간을 실속이는 등 등 몇 싼 계산을 추가하는 것보다 더 큰 영향을 미친다

최적화는 어려운 문제입니다 (책에 대한 전체 책을 쓰고 끝까지 얻을 수 없음). 특정 병목을 맹목적으로 최적화하는 데 시간을 보내는 것은 일반적으로 시간 낭비입니다. 프로파일 링이 없으면 최적화를 제대로 수행하기가 매우 어렵습니다.

그러나 경험상 맹인으로 무언가 를 할 필요가 있거나 일반적인 기본 전략으로 "메모리"를 최적화하는 것이 좋습니다.
"메모리"(특히 공간적 지역성 및 액세스 패턴)에 대한 최적화는 일반적으로 모든 것이 "동일한"시점이었던 것과 달리 현재 RAM에 액세스하는 것이 가장 비싼 것 중 하나이므로 (디스크에서 읽기가 부족합니다!) 원칙적으로 할 수 있습니다. 반면에 ALU는 저렴하고 매주 빨라지고 있습니다. 메모리 대역폭과 대기 시간은 거의 빠르게 향상되지 않습니다. 좋은 로컬 리티와 좋은 액세스 패턴은 데이터가 많은 응용 프로그램의 잘못된 액세스 패턴과 비교하여 런타임에서 5 배의 차이 (예를 들어 극단적 인 경우 20 배)를 쉽게 만들 수 있습니다. 당신의 캐시에 좋으면 행복한 사람이 될 것입니다.

이전 단락을 원근감있게 만들려면 수행 할 수있는 다양한 작업 비용을 고려하십시오. a+b하나 또는 두 개의 사이클이 걸리는 (최적화되지 않은 경우) 실행하는 것이 일반적이지만 CPU는 일반적으로 사이클 당 여러 개의 명령을 시작할 수 있으며 비 종속적 인 명령을 파이프 라인 할 수 있으므로보다 현실적인 방식으로 절반의 사이클 또는 그 이하의 비용이 소요됩니다. 이상적으로 컴파일러가 스케줄링에 능숙하고 상황에 따라 비용이 0이 될 수 있습니다.
데이터를 가져 오려면 ( "메모리") 운이 좋으면 L1에있는 경우 4 ~ 5 회, 운이 좋지 않으면 약 15 회 (L2 적중)가 소요됩니다. 데이터가 캐시에 전혀 없으면 수백주기가 걸립니다. 우연한 액세스 패턴이 TLB의 기능을 초과하는 경우 (~ 50 개의 항목만으로 쉽게 수행) 몇 백 사이클을 추가하십시오. 우연한 액세스 패턴으로 인해 실제로 페이지 오류가 발생하는 경우 가장 좋은 경우 수만주기, 최악의 경우 수백만주기가 소요됩니다.
이제 생각해보십시오. 가장 긴급하게 피하고 싶은 것은 무엇입니까?


0

메소드의 메모리 대 성능 속도를 언제 최적화해야합니까?

점점 후 기능을 바로 첫째 . 그런 다음 선택성 은 마이크로 최적화와 관련이 있습니다.


최적화에 관한 인터뷰 질문으로, 코드는 일반적인 토론을 불러 일으키지 만 코드가 기능적으로 정확합니까?

C ++ 및 C 및 기타 모두 int오버플로를의 문제로 간주 합니다 a + b. 잘 정의되어 있지 않으며 C는이를 정의되지 않은 동작 이라고합니다 . 일반적인 동작이지만 "포장"하도록 지정되지 않았습니다.

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

이러한 함수는 IsSumInRange()잘 정의되고의 모든 int값에 대해 올바르게 수행 될 것으로 예상됩니다 a,b. 원시 a + b는 아닙니다. AC 솔루션은 다음을 사용할 수 있습니다.

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

상기 코드는보다 넓은 정수형을 사용하여 최적화 될 수 int사용할 경우 다음으로, 또는 분배 sum > N, sum < -N내 테스트 if (a >= 0)로직. 그러나 이러한 최적화로 인해 스마트 컴파일러가 제공되는 "빠른"방출 코드가 실제로는 아니고 영리한 추가 유지 관리 가치가있는 것은 아닙니다.

  long long sum a;
  sum += b;

사용시에도 abs(sum)문제가 발생하기 쉽습니다 sum == INT_MIN.


0

우리는 어떤 종류의 컴파일러와 어떤 종류의 "메모리"를 이야기하고 있습니까? 귀하의 예에서 합리적인 최적화 프로그램을 가정하면 식은 a+b일반적으로 이러한 산술을 수행하기 전에 레지스터 (메모리 형식)에 저장해야합니다.

따라서 a+b두 번 발생하는 벙어리 컴파일러에 대해 이야기하는 경우 두 번째 예제 에서 더 많은 레지스터 (메모리)를 할당 합니다. 첫 번째 예제는 로컬 변수에 매핑 된 단일 레지스터에 해당 표현식을 한 번만 저장할 수 있기 때문입니다. 당신이 사방 모든 단일 변수를 유출 스택 바보 컴파일러 다른 종류의 작업을하지 않는 한 ...이 시점에서 매우 어리석은 컴파일러에 대해 이야기 다시 '하는 경우에 아마 첫 번째는 그것에게보다 최적화 더 많은 슬픔을 일으킬 것 두번째*.

나는 아직도 그 흠집을 할 그것은 세 가지 레지스터 할당 끝낼 수 있기 때문에 두 번째는, 그것이 유출을 쌓아 경향이 경우에도 바보 컴파일러와 함께 더 많은 메모리를 사용할 가능성이 생각 a+b유출과 ab더 많은. 우리가 캡처 한 후 가장 원시적 인 최적화를 이야기하는 경우 a+bs아마 "도움"것이다 덜 레지스터 / 스택 유출을 사용합니다.

이것은 측정 / 분해가없는 최악의 방식으로 극도로 추측 적이며 최악의 시나리오에서도 "메모리 대 성능"의 사례는 아닙니다 (내가 생각할 수있는 최악의 최적화 프로그램 중 하나라도 말하지 않고 있습니다) 스택 / 레지스터와 같은 임시 메모리를 제외하고는 순전히 "성능"사례이며, 합리적인 옵티 마이저 중에서 둘은 동일하며, 합리적인 옵티 마이저를 사용하지 않는 경우에는 매우 미세한 최적화에 대해 집착하는 이유는 무엇입니까? 특히 측정이 없습니까? 그것은 명령 선택 / 레지스터 할당 어셈블리 레벨 포커스와 같습니다. 예를 들어 스택을 통해 모든 것을 흘리는 통역사를 사용할 때 생산성을 유지하려는 사람은 결코 기대하지 않을 것입니다.

메소드의 메모리 대 성능 속도를 언제 최적화해야합니까?

이 질문에 대해 더 광범위하게 다룰 수 있다면 종종 두 개의 반대되는 것을 찾지 못합니다. 특히 액세스 패턴이 순차적이고 CPU 캐시 속도가 빠르면 사소하지 않은 입력에 대해 순차적으로 처리되는 바이트 수가 감소하여 해당 데이터를 더 빨리 쟁기질하는 경우가 많습니다. 물론 데이터가 훨씬 많은 방법으로, 더 많은 명령과 교환하여 훨씬 작 으면 명령이 적을수록 더 큰 형태로 순차적으로 처리하는 것이 더 빠를 수있는 단점이 있습니다.

그러나 많은 개발자들이 이러한 유형의 경우 메모리 사용을 줄이면 처리 시간이 비례 적으로 줄어드는 것을 과소 평가하는 경향이 있음을 발견했습니다. 일부 작은 계산 속도를 높이기 위해 일부 LUT의 큰 LUT에 도달하기까지 메모리 액세스가 아닌 명령어로 성능 비용을 변환하는 것은 매우 직관적으로 추가 메모리 액세스로 성능이 저하되는 경우가 있습니다.

거대한 배열 (예 : 로컬 스칼라 변수와 이야기하지 않음)을 통한 순차적 액세스 사례의 경우 순차적으로 쟁기질 할 메모리가 적을수록 성능이 향상됩니다. 특히 결과 코드가 그렇지 않은 경우보다 더 간단 할 때 't, 내 측정 및 프로파일 러가 달리 말해 줄 때까지 중요하며, 동일한 방식으로 디스크에서 작은 이진 파일을 순차적으로 읽는 것이 큰 파일보다 쟁기질하는 것이 더 빠를 것이라고 가정합니다 (작은 파일에 더 많은 지침이 필요한 경우에도) 그 가정이 더 이상 내 측정에 적용되지 않는 것으로 보일 때까지

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