Cocos2d-x 클라이언트의 랭킹 화면에 음수 점수가 떴다. 1 위가 마이너스 21 억이었다. 점수는 더하기만 하는데 어떻게 음수가 되는지가 첫 물음이었다.
Table of contents
Open Table of contents
범위를 넘겼다
점수가 int 였고 32 비트라 2147483647 까지 담는다. 그걸 넘으면 반대편 끝으로 넘어간다.
int a = 2147483647;
a = a + 1; // -2147483648
오류는 안 난다. 값이 조용히 바뀔 뿐이다. 콤보 배수가 붙는 스테이지에서 점수가 빠르게 올라갔고 오래 버틴 사람이 그 선을 넘었다.
찾아보니 부호 있는 정수가 넘치는 것은 C++ 표준이 정해 두지 않은 동작이었다. 대개 반대편으로 돌지만 그렇게 돈다고 기대하고 쓸 것은 아니었다. GCC 에 -fwrapv 를 주면 도는 것으로 고정된다.
원인은 배수 중복이었다
실제로 얼마까지 나올 수 있는지 계산해 봤다. enemy.txt 의 스테이지당 최대 점수와 최대 스테이지 수와 콤보 배수를 곱하니 2,500 만이었다. 21 억보다 훨씬 작은데 넘었다.
grep 으로 배수가 곱해지는 자리를 세니 하나 더 있었다. Stage.cpp 에서 한 번 곱하고 Result.cpp 의 합산에서 또 곱한다. 범위를 넘긴 것은 결과이고 원인은 배수 중복이었다.
자료형을 바꾸니 따라 봐야 할 곳이 있었다
배수 중복을 고치고 자료형도 int64_t 로 바꿨다. stdint.h 에 있다. 원인만 고치고 그대로 둘 수도 있지만 나중에 밸런스를 조정하면 다시 걸릴 수 있어 여유를 뒀다.
바꾸고 나니 따라 봐야 할 곳이 나왔다. fwrite 하는 코드가 sizeof(int) 로 크기를 재고 있어서 값이 잘렸다.
fwrite(&score, sizeof(score), 1, fp); // 변수 이름으로 잰다
서버로 보내는 sprintf 의 형식 지정자도 안 맞았다. %d 를 %lld 로 고쳐야 한다. 이걸 놓치면 값이 깨져서 나간다.
-Wall 을 켜면 GCC 가 잡아 준다.
warning: format specifies type 'int' but the argument has type 'int64_t'
경고를 끄고 있으면 못 본다. 이미 저장된 UserDefault.xml 은 옛 크기이므로 읽는 쪽에서 판을 확인하게 했다.
다른 곳과 확인 방법
같은 문제가 있을 만한 데를 grep 으로 찾았다. 계속 더하기만 하는 누적값과 배수가 붙는 값과 밀리초로 재는 시간이 후보였다.
각각 최댓값을 계산해 보니 대부분 int 로 충분했고 둘만 바꿨다. 전부 크게 잡는 것도 답은 아니다. 메모리와 저장 크기가 늘어난다.
더하기 전에 넘치는지 확인하는 코드는 안 넣었다. 계산상 못 넘기는데 넣으면 읽기만 어려워진다. 대신 COCOS2D_DEBUG 빌드에서 값이 음수가 되면 CCLOG 를 남기게 했다. 배수 중복을 찾은 것도 이 로그였다.
정리
- 정수는 담을 수 있는 범위가 있고 넘으면 조용히 반대편으로 넘어간다
- 오류가 안 나므로 값이 이상해진 뒤에야 안다
- 넘친 것은 결과다. 왜 그렇게 커졌는지를 먼저 본다
- 자료형을 바꾸면 저장과 전송과 출력 형식도 같이 봐야 한다
- 변수 이름으로 크기를 재면 자료형을 바꿔도 따라간다
- 형식 지정자가 안 맞으면 값이 깨진다.
-Wall이 그것을 잡는다 - 부호 있는 정수가 넘치는 것은 표준이 정해 둔 동작이 아니다
- 누적과 곱셈과 시간 계산은 커지기 쉬우니 최댓값을 계산한다
- 전부 크게 잡는 것도 답은 아니다