조건 검사와 같은 나눗셈이 필요할 때마다 나눗셈의 표현을 곱셈으로 리팩토링하고 싶습니다.
원본 버전 :
if(newValue / oldValue >= SOME_CONSTANT)
새로운 버전:
if(newValue >= oldValue * SOME_CONSTANT)
나는 그것이 피할 수 있다고 생각하기 때문에 :
0으로 나누기
oldValue
매우 작은 경우 오버플로
맞습니까? 이 습관에 문제가 있습니까?
조건 검사와 같은 나눗셈이 필요할 때마다 나눗셈의 표현을 곱셈으로 리팩토링하고 싶습니다.
원본 버전 :
if(newValue / oldValue >= SOME_CONSTANT)
새로운 버전:
if(newValue >= oldValue * SOME_CONSTANT)
나는 그것이 피할 수 있다고 생각하기 때문에 :
0으로 나누기
oldValue
매우 작은 경우 오버플로
맞습니까? 이 습관에 문제가 있습니까?
답변:
고려해야 할 두 가지 일반적인 경우 :
분명히 정수 산술을 사용하는 경우 (잘라지는) 다른 결과를 얻을 수 있습니다. 다음은 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 작업하고 있다면 아마 괜찮을 것입니다.
레거시 코드로 작업하고 있고 응용 프로그램이 산술을 수행하고 일관된 결과를 제공해야하는 재무 또는 기타 민감한 응용 프로그램 인 경우 작업을 변경할 때 매우주의해야합니다. 필요한 경우 산술의 미묘한 변화를 감지하는 단위 테스트가 있는지 확인하십시오.
배열의 요소 계산이나 다른 일반적인 계산 함수와 같은 일을하는 경우 괜찮을 것입니다. 그러나 곱셈 방법으로 코드를 더 명확하게 만들지 확실하지 않습니다.
알고리즘을 사양에 구현하는 경우 반올림 오류 문제뿐만 아니라 개발자가 코드를 검토하고 각 표현식을 사양에 다시 매핑하여 구현이 없는지 확인하기 위해 아무것도 변경하지 않습니다. 결함.
나는 많은 아이디어를 잠재적으로 다루기 때문에 귀하의 질문을 좋아합니다. 전체적으로, 나는 대답이 아마도 관련 유형과 특정 사례의 가능한 값 범위에 달려 있다고 생각합니다 .
내 본능은 스타일 을 반영하는 것 입니다 . 새 버전은 코드를 읽는 사람에게 명확하지 않습니다. 나는 새 버전의 의도를 결정하기 위해 1-2 초 (또는 아마도 더 긴 시간)를 생각해야하지만 이전 버전은 즉시 명확해야한다고 생각합니다. 가독성은 코드의 중요한 특성이므로 새 버전에는 비용이 발생합니다.
새 버전은 0으로 나누기를 피할 수 있습니다. 확실히 가드를 추가 할 필요는 없습니다 (의 행을 따라 if (oldValue != 0)
). 그러나 이것이 의미가 있습니까? 이전 버전은 두 숫자 사이의 비율을 반영합니다. 제수가 0이면 비율이 정의되지 않은 것입니다. 이는 상황에서 더 의미가있을 수 있습니다. 이 경우 결과를 생성하지 않아야합니다.
오버 플로우에 대한 보호는 논란의 여지가 있습니다. 그것이 newValue
항상보다 크다는 것을 알고 있다면 oldValue
아마도 그 주장을 할 수있을 것입니다. 그러나 (oldValue * SOME_CONSTANT)
오버플로 가 발생할 수 있습니다 . 그래서 나는 여기서 많은 이익을 보지 못합니다.
곱셈이 나누기보다 빠를 수 있기 때문에 (일부 프로세서에서는) 더 나은 성능을 얻을 수 있다는 주장이있을 수 있습니다. 그러나 상당한 이득을 얻기 위해서는 이와 같은 계산이 많이 필요합니다. 조기 최적화에주의하십시오.
위의 모든 것을 반영하면, 일반적으로 이전 버전과 비교하여 새 버전으로 얻을 것이 많지 않다고 생각합니다. 특히 선명도가 떨어졌습니다. 그러나 어떤 이점이있는 특정한 경우가있을 수 있습니다.
아니.
나는 아마 전화 것 조기 최적화 에 상관없이 최적화하고 있는지 여부, 넓은 의미에서, 성능 문구가 일반적으로 의미, 또는 다른 어떤 같은 최적화 할 수있는 것과 같은, 에지 카운트 , 코드의 라인을 , 또는 "디자인" 과 같은 것들이 훨씬 더 광범위 합니다.
이러한 종류의 최적화를 표준 운영 프로 시저로 구현하면 코드의 의미가 위험에 처하게되고 잠재적 으로 가장자리를 숨길 수 있습니다. 자동 제거에 적합한 엣지 케이스는 어쨌든 명시 적으로 해결해야 할 수도 있습니다 . 또한 자동으로 실패 하는 문제에 대해 잡음이있는 가장자리 (예외가 발생하는) 주변의 문제를 디버깅하는 것이 훨씬 더 쉽습니다 .
또한 경우에 따라 가독성, 명확성 또는 명시 성을 위해 "비 최적화"하는 것이 유리합니다. 대부분의 경우 사용자는 에지 처리 또는 예외 처리를 피하기 위해 몇 줄의 코드 또는 CPU주기를 저장 한 것을 알 수 없습니다. 반면에 어색하거나 조용히 실패한 코드 는 최소한 동료에게 영향을 미칩니다. 또한 소프트웨어를 구축하고 유지 관리하는 비용도 있습니다.
응용 프로그램의 도메인 및 특정 문제와 관련하여 더 "자연스럽고"읽을 수있는 것이 기본값입니다. 간단하고 명시 적이며 관용적으로 유지하십시오. 상당한 이익을 얻거나 합법적 인 사용성 임계 값을 달성하기 위해 필요에 따라 최적화하십시오 .
보통 , 제수는 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!
...
(ptr2 - ptr1) * 3 >= n
은 표현만큼 이해하기 쉬운 것을 찾으 ptr2 - ptr1 >= n / 3
십니까? 그것은 당신의 두뇌 여행을 넘어서지 않고 두 포인터 사이의 차이점을 삼키는 의미를 해독하려고 노력하지 않습니까? 그것이 당신과 당신의 팀에게 정말로 분명하다면, 당신에게 더 많은 힘을 줄 것입니다. 나는 단지 느린 소수에 있어야합니다.
n
와 임의의 숫자 3은 두 경우 모두 혼란 스럽지만 합리적인 이름으로 대체되어 하나 더 혼란스럽지 않습니다.
“가능한 한” 무엇이든 하는 것은 좋은 생각이 아닙니다 .
귀하의 최우선 순위는 정확성과 가독성 및 유지 보수성입니다. 가능할 때마다 맹목적으로 곱셈으로 곱셈을 바꾸는 경우가 종종 수정 부서에서 실패하는 경우가 종종 있으며, 드물기 때문에 경우를 찾기가 어렵습니다.
정확하고 읽기 쉬운 것을하십시오. 가장 읽기 쉬운 방식으로 코드를 작성하면 성능 문제가 발생한다는 확실한 증거가 있으면 코드 변경을 고려할 수 있습니다. 관리, 수학 및 코드 검토는 당신의 친구입니다.
코드 의 가독성 과 관련하여 어떤 경우에는 곱셈이 실제로 더 읽기 쉽다고 생각합니다 . 예를 들어, 경우 확인해야 뭔가가 newValue
5 % 이상 이상 증가 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로 평가됩니다 .
곱셈을 사용하는 불변 정수에 의한 유명한 논문 Division에 주목하십시오 .
정수가 변하지 않으면 컴파일러는 실제로 곱셈을 수행합니다! 부서가 아닙니다. 이것은 2의 제곱이 아닌 경우에도 발생합니다. 2 구간의 거듭 제곱은 분명히 비트 시프트를 사용하므로 훨씬 빠릅니다.
그러나 비 변형 정수의 경우 코드를 최적화하는 것은 사용자의 책임입니다. 실제 병목 현상을 실제로 최적화하고 있으며 정확성이 희생되지 않았 음을 최적화하기 전에 확인하십시오. 정수 오버 플로우에주의하십시오.
마이크로 최적화에 관심이 있으므로 최적화 가능성을 살펴볼 것입니다.
코드가 실행되는 아키텍처에 대해서도 생각하십시오. 특히 ARM은 분할 속도가 매우 느립니다. 나누기 위해 함수를 호출해야하며 ARM에는 나누기 명령이 없습니다.
I은 또한 32 비트 아키텍쳐에서, 64 비트 분할은 최적화되지 알았다 .
포인트 2에서 픽업하면 실제로 오버플로를 방지 할 수 oldValue
있습니다. 그러나 SOME_CONSTANT
매우 작은 경우 대체 방법은 값을 정확하게 표현할 수없는 언더 플로로 끝납니다.
반대로, oldValue
매우 큰 경우 어떻게됩니까 ? 당신은 똑같은 문제가 있습니다.
오버플로 / 언더 플로의 위험을 피하거나 최소화하려면 가장 좋은 방법은 newValue
크기가에 가까운 지 oldValue
또는에 가장 가까운 지를 확인하는 것 입니다 SOME_CONSTANT
. 그런 다음 적절한 나누기 작업을 선택할 수 있습니다.
if(newValue / oldValue >= SOME_CONSTANT)
또는
if(newValue / SOME_CONSTANT >= oldValue)
결과는 가장 정확합니다.
0으로 나누기의 경우, 내 경험상 이것은 수학에서 "해결"하기에 거의 적합하지 않습니다. 연속 검사에서 0으로 나누기가 있다면 거의 확실하게 분석이 필요한 상황이 있으며이 데이터를 기반으로 한 계산은 의미가 없습니다. 0으로 명시 적으로 나누는 검사는 거의 항상 적절한 이동입니다. (여기서 나는 "거의"라고 말하는데, 그 이유는 난폭하다고 주장하지 않기 때문입니다. 저는 임베디드 소프트웨어를 작성하는 20 년 동안 이것에 대한 정당한 이유를보고 기억하지 못합니다. .)
그러나 응용 프로그램에서 오버플로 / 언더 플로가 발생할 위험이 있다면 이것이 올바른 해결책이 아닐 수도 있습니다. 일반적으로 알고리즘의 수치 안정성을 확인하거나 더 높은 정밀도 표현으로 이동해야합니다.
오버플로 / 언더 플로의 위험이 입증되지 않은 경우 아무것도 걱정하지 않아도됩니다. 그것은 말 그대로 코드 옆에 주석으로 숫자가 필요하다는 것을 말 그대로 관리자에게 설명하는 이유를 설명해야 한다는 것을 의미합니다 . 다른 사람들의 코드를 검토하는 수석 엔지니어로서,이 문제를 해결하기 위해 다른 사람을 만나면 개인적으로 더 적은 것을 받아들이지 않을 것입니다. 이것은 조기 최적화와는 정반대의 것이지만 일반적으로 동일한 근본 원인이 있습니다. 세부적인 부분에 대한 집착은 기능상의 차이가 없습니다.
CPU의 ALU (Arithmetic-Logic Unit)는 하드웨어로 구현되었지만 알고리즘을 실행하기 때문에 곱셈을 나누기로 대체하는 것은 좋은 생각이 아닙니다. 최신 프로세서에서는보다 정교한 기술을 사용할 수 있습니다. 일반적으로 프로세서는 필요한 클록주기를 최소화하기 위해 비트 쌍 작업을 병렬화하려고합니다. 곱셈 알고리즘은 상당히 효과적으로 병렬화 될 수 있습니다 (더 많은 트랜지스터가 필요하지만). 분할 알고리즘은 효율적으로 병렬화 할 수 없습니다. 가장 효율적인 나눗셈 알고리즘은 매우 복잡합니다. 일반적으로 비트 당 더 많은 클럭 사이클이 필요합니다.
oldValue >= 0
합니까?