가능하면 나누기를 곱셈으로 바꾸는 것이 좋은 방법입니까?


73

조건 검사와 같은 나눗셈이 필요할 때마다 나눗셈의 표현을 곱셈으로 리팩토링하고 싶습니다.

원본 버전 :

if(newValue / oldValue >= SOME_CONSTANT)

새로운 버전:

if(newValue >= oldValue * SOME_CONSTANT)

나는 그것이 피할 수 있다고 생각하기 때문에 :

  1. 0으로 나누기

  2. oldValue매우 작은 경우 오버플로

맞습니까? 이 습관에 문제가 있습니까?


41
음수를 사용하면 두 버전이 완전히 다른 것을 확인합니다. 확실 oldValue >= 0합니까?
user2313067

37
당신은, 컴파일러, 더 잘 할 일반적으로 수 있습니다 생각할 수있는 어떤 최적화 언어 (그러나 특히 C와)에 따라 또는 - 전혀 그것을 할 수있는 충분한 의미가있다.
Mark Benningfield

63
하는 "좋은 연습"결코 항상 X와 Y는 의미 적으로 동등하지 않을 때 코드를 Y에 의해 코드 X를 대체합니다. 그러나 항상 X와 Y를보고, 두뇌를 켜고 , 요구 사항이 무엇인지 생각한 다음, 두 가지 대안 중 어느 것이 더 정확한지 결정하는 것이 좋습니다. 그 후에 의미 차이가 올바른지 확인하기 위해 어떤 테스트가 필요한지 고려해야합니다.
Doc Brown

12
@MarkBenningfield : 컴파일러는 0으로 나누기를 최적화 할 수 없습니다. 당신이 생각하는 "최적화"는 "속도 최적화"입니다. OP는 또 다른 종류의 최적화, 즉 버그 회피에 대해 생각하고 있습니다.
slebetman

25
포인트 2는 가짜입니다. 작은 값의 경우 원본 버전이 오버플로 될 수 있지만 큰 값의 경우 새 버전이 오버플로 될 수 있으므로 일반적인 경우 어느 쪽도 더 안전하지 않습니다.
JacquesB

답변:


74

고려해야 할 두 가지 일반적인 경우 :

정수 산술

분명히 정수 산술을 사용하는 경우 (잘라지는) 다른 결과를 얻을 수 있습니다. 다음은 C #의 작은 예입니다.

public static void TestIntegerArithmetic()
{
    int newValue = 101;
    int oldValue = 10;
    int SOME_CONSTANT = 10;

    if(newValue / oldValue > SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue > oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

산출:

First comparison says it's not bigger.
Second comparison says it's bigger.

부동 소수점 산술

나누기가 0으로 나눠 질 때 나누기가 다른 결과를 산출 할 수 있다는 사실 (제외는 발생하지 않지만 예외는 발생 함) 외에도 약간 다른 반올림 오차와 다른 결과가 발생할 수 있습니다. C #의 간단한 예 :

public static void TestFloatingPoint()
{
    double newValue = 1;
    double oldValue = 3;
    double SOME_CONSTANT = 0.33333333333333335;

    if(newValue / oldValue >= SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue >= oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

산출:

First comparison says it's not bigger.
Second comparison says it's bigger.

당신이 나를 믿지 않는 경우, 여기 당신이 직접 실행하고 볼 수 있는 바이올린 이 있습니다.

다른 언어는 다를 수 있습니다. 그러나 C #은 다른 언어와 마찬가지로 IEEE 표준 (IEEE 754) 부동 소수점 라이브러리를 구현하므로 다른 표준화 된 런타임에서도 동일한 결과를 얻을 수 있습니다.

결론

greenfield 작업하고 있다면 아마 괜찮을 것입니다.

레거시 코드로 작업하고 있고 응용 프로그램이 산술을 수행하고 일관된 결과를 제공해야하는 재무 또는 기타 민감한 응용 프로그램 인 경우 작업을 변경할 때 매우주의해야합니다. 필요한 경우 산술의 미묘한 변화를 감지하는 단위 테스트가 있는지 확인하십시오.

배열의 요소 계산이나 다른 일반적인 계산 함수와 같은 일을하는 경우 괜찮을 것입니다. 그러나 곱셈 방법으로 코드를 더 명확하게 만들지 확실하지 않습니다.

알고리즘을 사양에 구현하는 경우 반올림 오류 문제뿐만 아니라 개발자가 코드를 검토하고 각 표현식을 사양에 다시 매핑하여 구현이 없는지 확인하기 위해 아무것도 변경하지 않습니다. 결함.


41
두 번째 금융 비트. 이런 종류의 스위치는 회계사에게 갈퀴를 쫓아달라고 요청합니다. 나는 "올바른"대답을 찾는 것보다 갈림길을 막기 위해 더 많은 노력을 기울여야했던 5,000 줄을 기억합니다. 실제로는 일반적으로 약간 잘못되었습니다. .01 % 가량 떨어져도 상관 없습니다. 절대적으로 일관된 답변이 필수였습니다. 따라서 체계적인 반올림 오류가 발생하는 방식으로 계산을 수행해야했습니다.
Loren Pechtel

8
5 센트 사탕을 구입하는 것을 생각하십시오 (더 이상 존재하지 않음). 20 조각을 구입하십시오. 한 조각의 20 구입에 대해 세금이 없기 때문에 "정답"은 세금이 아닙니다.
Loren Pechtel

24
@LorenPechtel은 대부분의 세금 시스템이 거래 당 세금이 부과되는 규칙을 포함하고 있기 때문에 세금은 해당 영역의 가장 작은 동전보다 조금씩 증가하며 세금 금액은 납세자의 호의로 반올림됩니다. 이러한 규칙은 합법적이고 일관성이 있기 때문에 "올바른"규칙입니다. 갈퀴를 가진 회계사들은 아마도 컴퓨터 프로그래머들이 경험하지 않는 한, 규칙이 실제로 컴퓨터 프로그래머들이하지 않는 방식을 알고있을 것입니다. 0.01 % 오류는 밸런싱 오류를 일으킬 가능성이 있으며 밸런싱 오류가있는 것은 불법입니다.
Steve

9
그린 필드 라는 용어를 들어 본 적이 없기 때문에 찾아 보았습니다. Wikipedia는 "이전 작업에 의해 부과 된 제약이없는 프로젝트"라고 말합니다.
Henrik Ripa

9
@Steve : 최근 상사는 "greenfield"와 "brownfield"를 대조했습니다. 나는 특정 프로젝트가 "블랙 필드"와 더 비슷하다고 언급했다 ... :-D
DevSolar

25

나는 많은 아이디어를 잠재적으로 다루기 때문에 귀하의 질문을 좋아합니다. 전체적으로, 나는 대답이 아마도 관련 유형과 특정 사례의 가능한 값 범위에 달려 있다고 생각합니다 .

내 본능은 스타일 을 반영하는 것 입니다 . 새 버전은 코드를 읽는 사람에게 명확하지 않습니다. 나는 새 버전의 의도를 결정하기 위해 1-2 초 (또는 아마도 더 긴 시간)를 생각해야하지만 이전 버전은 즉시 명확해야한다고 생각합니다. 가독성은 코드의 중요한 특성이므로 새 버전에는 비용이 발생합니다.

새 버전은 0으로 나누기를 피할 수 있습니다. 확실히 가드를 추가 할 필요는 없습니다 (의 행을 따라 if (oldValue != 0)). 그러나 이것이 의미가 있습니까? 이전 버전은 두 숫자 사이의 비율을 반영합니다. 제수가 0이면 비율이 정의되지 않은 것입니다. 이는 상황에서 더 의미가있을 수 있습니다. 이 경우 결과를 생성하지 않아야합니다.

오버 플로우에 대한 보호는 논란의 여지가 있습니다. 그것이 newValue항상보다 크다는 것을 알고 있다면 oldValue아마도 그 주장을 할 수있을 것입니다. 그러나 (oldValue * SOME_CONSTANT)오버플로 가 발생할 수 있습니다 . 그래서 나는 여기서 많은 이익을 보지 못합니다.

곱셈이 나누기보다 빠를 수 있기 때문에 (일부 프로세서에서는) 더 나은 성능을 얻을 수 있다는 주장이있을 수 있습니다. 그러나 상당한 이득을 얻기 위해서는 이와 같은 계산이 많이 필요합니다. 조기 최적화에주의하십시오.

위의 모든 것을 반영하면, 일반적으로 이전 버전과 비교하여 새 버전으로 얻을 것이 많지 않다고 생각합니다. 특히 선명도가 떨어졌습니다. 그러나 어떤 이점이있는 특정한 경우가있을 수 있습니다.


16
흠, 임의의 곱셈이 임의의 분할보다 더 효율적이라는 것은 실제 기계에서 실제로 프로세서에 의존하지 않습니다.
중복 제거기

1
정수 대 부동 소수점 산술 문제도 있습니다. 비율이 소수 인 경우 분할을 부동 소수점으로 수행해야하며 캐스트가 필요합니다. 캐스트가 없으면 의도하지 않은 실수가 발생합니다. 분수가 두 개의 작은 정수 사이의 비율 인 경우,이를 재정렬하면 정수 산술에서 비교를 수행 할 수 있습니다. (이 시점에서 귀하의 주장이 적용될 것입니다.)
rwong

@rwong 항상 그런 것은 아닙니다. 소수의 언어는 소수 부분을 제거하여 정수 나누기를 수행하므로 캐스트가 필요하지 않습니다.
T. Sar

@ T.Sar 설명하는 기술과 대답에 설명 된 의미가 다릅니다. 의미론은 프로그래머가 답을 부동 소수점 또는 분수 값으로 의도하는지 여부입니다. 당신이 묘사하는 기술은 상호 곱셈에 의한 나눗셈이며, 때로는 정수 나누기에 대한 완벽한 근사치 (대체)입니다. 후자의 기술은 일반적으로 제수를 미리 알고있을 때 적용되는데, 정수 역수의 유도 (2 ** 32만큼 이동)는 컴파일 타임에 수행 될 수 있기 때문입니다. 런타임에 그렇게하면 CPU 비용이 많이 들기 때문에 유익하지 않습니다.
rwong

22

아니.

나는 아마 전화 것 조기 최적화 에 상관없이 최적화하고 있는지 여부, 넓은 의미에서, 성능 문구가 일반적으로 의미, 또는 다른 어떤 같은 최적화 할 수있는 것과 같은, 에지 카운트 , 코드의 라인을 , 또는 "디자인" 과 같은 것들이 훨씬 더 광범위 합니다.

이러한 종류의 최적화를 표준 운영 프로 시저로 구현하면 코드의 의미가 위험에 처하게되고 잠재적 으로 가장자리를 숨길 수 있습니다. 자동 제거에 적합한 엣지 케이스는 어쨌든 명시 적으로 해결해야 할 수도 있습니다 . 또한 자동으로 실패 하는 문제에 대해 잡음이있는 가장자리 (예외가 발생하는) 주변의 문제를 디버깅하는 것이 훨씬 더 쉽습니다 .

또한 경우에 따라 가독성, 명확성 또는 명시 성을 위해 "비 최적화"하는 것이 유리합니다. 대부분의 경우 사용자는 에지 처리 또는 예외 처리를 피하기 위해 몇 줄의 코드 또는 CPU주기를 저장 한 것을 알 수 없습니다. 반면에 어색하거나 조용히 실패한 코드 최소한 동료에게 영향을 미칩니다. 또한 소프트웨어를 구축하고 유지 관리하는 비용도 있습니다.

응용 프로그램의 도메인 및 특정 문제와 관련하여 더 "자연스럽고"읽을 수있는 것이 기본값입니다. 간단하고 명시 적이며 관용적으로 유지하십시오. 상당한 이익을 얻거나 합법적 인 사용성 임계 값을 달성하기 위해 필요에 따라 최적화하십시오 .

또한 참고 : 컴파일러는 안전 할 때 언제라도 나눗셈최적화 합니다.


11
-1이 답변은 나눗셈의 잠재적 함정에 관한 질문에 실제로 맞지 않습니다-최적화와 관련이 없음
Ben Cottrell

13
@ BenCottrell 그것은 완벽하게 잘 맞습니다. 함정은 유지 관리 비용으로 무의미한 성능 최적화에 가치를 두는 것입니다. 질문에서 "이 습관에 문제가 있습니까?" - 예. 그것은 절대 횡설수설로 빠르게 이어질 것이다.
Michael

9
@Michael 질문은 그중 어떤 것에 대해서도 묻지 않습니다. 특히 각각 다른 의미와 동작을 가진 두 가지 다른 표현 의 정확성 에 대해 묻고 있지만 둘 다 동일한 요구 사항에 맞도록 고안되었습니다.
벤 Cottrell

5
@ BenCottrell 아마도 당신은 질문에서 정확성에 대해 언급 할 부분이 어디 있는지 지적 할 수 있습니까?
Michael

5
@ BenCottrell 당신은 방금 '나는 할 수 없다'고 말했을 것입니다 :)
Michael

13

버그가 적고 논리적으로 이해되는 것을 사용하십시오.

보통 , 제수는 0이 될 수 있기 때문에 변수로 나누는 것은 나쁜 생각입니다.
상수로 나누는 것은 일반적으로 논리적 의미가 무엇인지에 달려 있습니다.

상황에 따라 표시되는 몇 가지 예는 다음과 같습니다.

디비전 양호 :

if ((ptr2 - ptr1) >= n / 3)  // good: check if length of subarray is at least n/3
    ...

곱셈 불량 :

if ((ptr2 - ptr1) * 3 >= n)  // bad: confusing!! what is the intention of this code?
    ...

곱셈 양호 :

if (j - i >= 2 * min_length)  // good: obviously checking for a minimum length
    ...

나눗셈 불량 :

if ((j - i) / 2 >= min_length)  // bad: confusing!! what is the intention of this code?
    ...

곱셈 양호 :

if (new_length >= old_length * 1.5)  // good: is the new size at least 50% bigger?
    ...

나눗셈 불량 :

if (new_length / old_length >= 2)  // bad: BUGGY!! will fail if old_length = 0!
    ...

2
문맥에 따라 다르지만 처음 두 쌍의 예제는 극도로 열악합니다. 어느 경우 든 나는 다른 것을 선호하지 않을 것이다.
Michael

6
@ 마이클 : 음 ... 당신 (ptr2 - ptr1) * 3 >= n은 표현만큼 이해하기 쉬운 것을 찾으 ptr2 - ptr1 >= n / 3십니까? 그것은 당신의 두뇌 여행을 넘어서지 않고 두 포인터 사이의 차이점을 삼키는 의미를 해독하려고 노력하지 않습니까? 그것이 당신과 당신의 팀에게 정말로 분명하다면, 당신에게 더 많은 힘을 줄 것입니다. 나는 단지 느린 소수에 있어야합니다.
Mehrdad

2
호출 된 변수 n와 임의의 숫자 3은 두 경우 모두 혼란 스럽지만 합리적인 이름으로 대체되어 하나 더 혼란스럽지 않습니다.
Michael

1
이 예는 실제로 나쁘지 않습니다. 확실히 '아주 가난'하지는 않습니다. '합리적인 이름'에 속하더라도 나쁜 경우로 바꾸면 여전히 의미가 떨어집니다. 프로젝트를 처음 접했다면 프로덕션 코드를 수정하려고 할 때이 답변에 나열된 '좋은'사례를 많이 보았습니다.
John-M

3

“가능한 한” 무엇이든 하는 것은 좋은 생각이 아닙니다 .

귀하의 최우선 순위는 정확성과 가독성 및 유지 보수성입니다. 가능할 때마다 맹목적으로 곱셈으로 곱셈을 바꾸는 경우가 종종 수정 부서에서 실패하는 경우가 종종 있으며, 드물기 때문에 경우를 찾기가 어렵습니다.

정확하고 읽기 쉬운 것을하십시오. 가장 읽기 쉬운 방식으로 코드를 작성하면 성능 문제가 발생한다는 확실한 증거가 있으면 코드 변경을 고려할 수 있습니다. 관리, 수학 및 코드 검토는 당신의 친구입니다.


1

코드 의 가독성 과 관련하여 어떤 경우에는 곱셈이 실제로 읽기 쉽다고 생각합니다 . 예를 들어, 경우 확인해야 뭔가가 newValue5 % 이상 이상 증가 oldValue하고 1.05 * oldValue테스트 할 수에 대한 임계 값 newValue, 쓸 자연

    if (newValue >= 1.05 * oldValue)

그러나 이런 식으로 리팩토링 할 때 음수를주의하십시오 (나눗셈을 곱셈으로 바꾸거나 곱셈을 나누기로 바꿉니다). oldValue네거티브가 아니라고 확인되면 두 조건이 동일합니다 . 그러나 newValue실제로는 -13.5이고 oldValue-10.1 이라고 가정 합니다. 그때

newValue/oldValue >= 1.05

true로 평가 되지만

newValue >= 1.05 * oldValue

false로 평가됩니다 .


1

곱셈을 사용하는 불변 정수에 의한 유명한 논문 Division에 주목하십시오 .

정수가 변하지 않으면 컴파일러는 실제로 곱셈을 수행합니다! 부서가 아닙니다. 이것은 2의 제곱이 아닌 경우에도 발생합니다. 2 구간의 거듭 제곱은 분명히 비트 시프트를 사용하므로 훨씬 빠릅니다.

그러나 비 변형 정수의 경우 코드를 최적화하는 것은 사용자의 책임입니다. 실제 병목 현상을 실제로 최적화하고 있으며 정확성이 희생되지 않았 음을 최적화하기 전에 확인하십시오. 정수 오버 플로우에주의하십시오.

마이크로 최적화에 관심이 있으므로 최적화 가능성을 살펴볼 것입니다.

코드가 실행되는 아키텍처에 대해서도 생각하십시오. 특히 ARM은 분할 속도가 매우 느립니다. 나누기 위해 함수를 호출해야하며 ARM에는 나누기 명령이 없습니다.

I은 또한 32 비트 아키텍쳐에서, 64 비트 분할은 최적화되지 알았다 .


1

포인트 2에서 픽업하면 실제로 오버플로를 방지 할 수 oldValue있습니다. 그러나 SOME_CONSTANT매우 작은 경우 대체 방법은 값을 정확하게 표현할 수없는 언더 플로로 끝납니다.

반대로, oldValue매우 큰 경우 어떻게됩니까 ? 당신은 똑같은 문제가 있습니다.

오버플로 / 언더 플로의 위험을 피하거나 최소화하려면 가장 좋은 방법은 newValue크기가에 가까운 지 oldValue또는에 가장 가까운 지를 확인하는 것 입니다 SOME_CONSTANT. 그런 다음 적절한 나누기 작업을 선택할 수 있습니다.

    if(newValue / oldValue >= SOME_CONSTANT)

또는

    if(newValue / SOME_CONSTANT >= oldValue)

결과는 가장 정확합니다.

0으로 나누기의 경우, 내 경험상 이것은 수학에서 "해결"하기에 거의 적합하지 않습니다. 연속 검사에서 0으로 나누기가 있다면 거의 확실하게 분석이 필요한 상황이 있으며이 데이터를 기반으로 한 계산은 의미가 없습니다. 0으로 명시 적으로 나누는 검사는 거의 항상 적절한 이동입니다. (여기서 나는 "거의"라고 말하는데, 그 이유는 난폭하다고 주장하지 않기 때문입니다. 저는 임베디드 소프트웨어를 작성하는 20 년 동안 이것에 대한 정당한 이유를보고 기억하지 못합니다. .)

그러나 응용 프로그램에서 오버플로 / 언더 플로가 발생할 위험이 있다면 이것이 올바른 해결책이 아닐 수도 있습니다. 일반적으로 알고리즘의 수치 안정성을 확인하거나 더 높은 정밀도 표현으로 이동해야합니다.

오버플로 / 언더 플로의 위험이 입증되지 않은 경우 아무것도 걱정하지 않아도됩니다. 그것은 말 그대로 코드 옆에 주석으로 숫자가 필요하다는 것을 말 그대로 관리자에게 설명하는 이유를 설명해야 한다는 것을 의미합니다 . 다른 사람들의 코드를 검토하는 수석 엔지니어로서,이 문제를 해결하기 위해 다른 사람을 만나면 개인적으로 더 적은 것을 받아들이지 않을 것입니다. 이것은 조기 최적화와는 정반대의 것이지만 일반적으로 동일한 근본 원인이 있습니다. 세부적인 부분에 대한 집착은 기능상의 차이가 없습니다.


0

의미있는 방법과 속성으로 조건부 산술을 캡슐화합니다. 좋은 이름은 "A / B"가 의미 하는 바를 알려줄뿐만 아니라 매개 변수 검사 및 오류 처리도 깔끔하게 숨길 수 있습니다.

중요한 것은, 이러한 방법들이보다 복잡한 논리로 구성되어 있기 때문에 외적 복잡성은 매우 관리하기 쉽다는 것입니다.

곱셈 치환은 문제가 잘못 정의되어 있기 때문에 합리적인 해결책으로 보인다고 말하고 싶습니다.


0

CPU의 ALU (Arithmetic-Logic Unit)는 하드웨어로 구현되었지만 알고리즘을 실행하기 때문에 곱셈을 나누기로 대체하는 것은 좋은 생각이 아닙니다. 최신 프로세서에서는보다 정교한 기술을 사용할 수 있습니다. 일반적으로 프로세서는 필요한 클록주기를 최소화하기 위해 비트 쌍 작업을 병렬화하려고합니다. 곱셈 알고리즘은 상당히 효과적으로 병렬화 될 수 있습니다 (더 많은 트랜지스터가 필요하지만). 분할 알고리즘은 효율적으로 병렬화 할 수 없습니다. 가장 효율적인 나눗셈 알고리즘은 매우 복잡합니다. 일반적으로 비트 당 더 많은 클럭 사이클이 필요합니다.

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