~21210

특허를 받고도 아직도 부족한 점이 많아 보여 사업화는 하지 못했다. 너무 간단하게 만들다 보니 키 개수가 적었고 익숙함을 요구하여 대중화하기도 어렵다. 그래서 새로 만들기로 하고 이런 저런 생각만 많았을 뿐 실천에 옮기지 못했다. 그러다가 개인적인 큰 일이 있어 몇 년 방황하거나 신나게 놀다가 정신을 차려 보니 세월을 훌쩍 지나가 있었고 뒤쳐진 나를 추스리느라 또 얼마간의 시간이 흘렀다.

그러다가 19년 여름에 취직해서 회사를 다니게 되었는데 먹고 사는 건 해결이 되었지만 맨날 똑같은 일상이 지겨워지기 시작했다. 20년 초에는 회사는 다니더라도 뭔가 의미있는 일을 슬슬 시작해야겠다는 결심을 했다. 그리고 1년 내도록 키보드를 어떻게 만들지, 다른 키보드는 어떤지 관심을 가지고 구경하고 연구하기 시작했다.

이런 저런 키보드를 수집해 보고 장단점을 따져 보고 좋은 키보드의 요건을 하나씩 정리해 두었다. 20년 후반기에는 SQL 책을 집필하느라 연구만 했지 막상 실천에 옮기지 못했다. 21년 초 이제 책 작업도 거의 다 되어 가고 있어 슬슬 다시 연구를 시작하려고 한다. 우선 9년전에 내가 무슨 생각을 하고 있었는지부터 차근히 되짚어 보고 연구성과부터 계승할 생각이다. 그리고 새로 연구한 것들을 하나씩 적용해 볼 것이다. 오늘은 여기까지.

2137- 과거 작업 파악

SQL 정복 교정 작업 및 회사일로 바빠 신경쓸 겨를이 없다가 마침 출판사 교정이 지연되는 일이 발생하여 36, 7 이틀간 키보드 기획을 할 기회가 생겼다. 가장 시급한 일은 9년전에 내가 어디까지 작업을 해 놨고 무엇이 문제인가를 파악하는 일이다. 이전의 일지를 차근 차근히 읽어 보고 상황 파악부터 했다. 요약해 보면 다음과 같다.

 

- 모드가 너무 많으면 불편하고 익숙해지기 어려워 대중화도 어렵다. 쿼티는 언어/Shift 이외의 모드가 따로 없다. 모드 변환없이 단독 입력이 가능해야 하며 결국 더 많은 키가 필요하다. 과하게 압축하지 말고 1행과 집안열을 공식 도입하는게 낫다.

- Upper 키는 키 하나에 2개의 문자를 할당한다. Shift와는 달리 조합이 아니라 연이어 누르는 방식이라 사용법이 다르고 언어별로 필요성이 달라 문자 영역에 배치했다. 그러나 모든 언어가 24개 문자는 넘는 편이라 엄지영역으로 이동해 두었는데 키가 늘어나면 이 키가 딱히 필요 없을 거 같다. 타자는 손가락이 치는 것이지 두되의 개입을 최소화해야 한다.

- 대중성과 편의성을 위해 2벌식과의 호환성을 고려하지 않을 수 없다. 제자리 투터치보다는 근거리 원터치가 더 낫다. 편의성과 제자리는 배치된다. 한글, 영어, 숫자, 기호 등이 섞여 나타날 때 익숙해지는데 너무 오랜 시간이 걸려 초반에 관심을 받을 수가 없다. ㅇ ㅏ 둘 다 홈포지션을 벗어나 아너림 명칭을 포기하더라도 초기 대중화를 위해 타협이 필요하다.

- 숫자를 1행에 고정할지 아니면 Num 모드로 사각형으로 배치할지는 아직 결정하지 못했다. 각각 장단점이 있어 결정하기 어렵다. 1행에 놓으면 안보고 칠 수 없고 사각형으로 놓으면 모드 전환이 필요하다.

 

키보드 배치도는 201274일자, 713일자 두 가지가 있는데 어떤게 더 최신인지도 잘 모르겠다. 그래서 기존 고민 내용을 보고 아예 다시 배치도를 그렸다. 모드는 언어(/), 숫자, 편집 4가지가 있고 각 모드에 따른 키 배치를 다음과 같이 초안 작성했다.

키를 4분할하여 좌상은 영어, 우상은 한글, 우좌는 숫자, 우하는 편집 모드이다. 기호는 모드에 거의 영향을 받지 않으며 Shift 문자와 Normal 문자를 나란히 표현했다. 총 키수 82이며 53키는 필수이고 F, 편집키 24키는 옵션이다. 9년전 기획에 비해 다음과 같은 변형을 주었다.

 

- 1행과 집안열 모두 포함 - 키 개수를 충분히 확보하여 자주 쓰는 기호는 다 포함시켰다. 이렇게 하고도 숫자와 관련된 +-*/`~\| 기호 8개는 숫자 모드에 있어 불편함이 있다.

- 알파벳 Z, Q를 집안 윗행에서 아래행으로 이동했다. 위보다는 아래가 더 접근성이 좋다. 한글도 집안열을 가급적 활용할 계획이다.

- 숫자 모드의 숫자를 한칸 위로 올렸다. 공백과 소수점(.) 쉼표(,)를 그대로 쓸 수 있는 이점이 있고 직관적이다. 다만 기호 영역의 위 3키를 침범하여 (){<>[ 괄호를 입력할 때는 잠시 모드를 바꾸어야 한다.

- 편집키 위쪽에 PrtSc, Ins, Scr 키를 배치했다. Pause는 잘 안쓰니 단독키는 아니다. InsDel 위에 배치하여 직관성을 높였다. 편집 모드의 방향키도 한칸 위로 올려 좌우 이동키를 홈 포지션에 맞추었다. 단 위쪽 세 편집키가 4행으로 내려와 좀 어색하다.

- Esc, Tab을 왼새밖열에 배치하고 Caps Lock도 단독키로 주었다. 자리가 좀 아깝고 영문에만 쓰는 키이지만 유럽이나 다른 언어에서도 일반적으로 많이 쓰는데다 원래 있던 키이니 그 자리에 배치하는 것이 맞는 거 같다.

 

여기까지 만들어 놓고 보니 기호 8개가 숫자 모드에 있는게 마음에 안든다. 그리고 왼손은 새밖열이 있는데 비해 오른쪽은 없어 비대칭이다. 기존 쿼티 키보드도 -=[]\' 기호가 오른손 새끼 영역에 있으니 오른쪽 새밖열을 두어도 별 이상이 없을 거 같다. 문자가 더 많은 언어를 위해 당장 안 쓰더라도 문자키 영역을 충분히 확보해 놓는 것도 의미가 있을 거 같아 새밖열을 공식적으로 도입하고 기호키를 재배치했다.

58키에 24키 더해서 82키이되 옵션키를 빼면 58키로도 모든 입력, 편집이 가능하다. 정확히 좌우 대칭이고 모든 기호가 단독키에 있어 편의성이 높다. 다음 사항이 변경되었다.

 

- 새밖열 4개 기호키 추가하고 연산자 위주로 배치함. 연산자가 한칸 떨어지는게 좀 아쉬운데 문자 모드에서는 새끼 손가락 열로 이동시켜도 될 거 같다.

- 왼쪽 새끼 4행에 FN키 배치. 이 키는 당장은 기능이 없으며 디바이스 드라이버는 입력을 직접 받지 않는다. 키보드가 자체적으로 블루투스 페어링, LED 조명 조정 등 자체적인 기능을 구현할 때 사용하라고 미리 만들어 둔 것이다. 용도는 메이커가 알아서 정하되 위치라도 고정해 두면 사용자 편의성이 향상된다.

- EditNum의 키 위치를 바꾸었다. Num은 왼쪽키영역이 다 비어 있어 왼손 약지로 누른채 쓰면 된다. 반면 Edit는 왼쪽 영역에 펑션키가 있어 약지로 눌러서는 같은 손가락 위치인 F2, F6, F10을 그냥은 칠 수 없는 문제가 있다.

- 집안열에 한글 경음과 ㅑㅕㅛ를 맵핑했다. 제자리에서 약간 벗어나고 입력방법이 일관되지 못한 문제가 있지만 Shift나 더블푸시를 줄일 수 있어 입력속도 향상에 도움이 된다. , , ㅉ은 단독키가 없어 두번 누르거나 Shift키를 사용해야 한다.

 

이 기획안에서 기호키와 편집키의 위치 등은 좀 더 세부 조정이 필요하다. 왼쪽에 매크로키나 Copy, Paste 키를 놓는 것도 좀 고려해 볼만하다.

2139: 문자 배치 수정

문자 배치를 수정했다.

 

- 오른쪽 새밖열의 연산자 위치 조정. 숫자를 입력할 때 한칸 위인 456열에 손가락을 두므로 가장 많이 쓰는 +를 중앙에, 위에 -를 배치했다. /는 웹 주소 입력, 프로그래밍 주석 입력 등에 많이 사용하므로 제일 위보다는 문자 모드에서 접근하기 쉬운 아래쪽으로 이동했다.

- 숫자 모드에 새밖열의 기호 8개를 새끼열, 집안열에 중복 배치한다. 숫자와 연산자를 제자리에서 칠 수 있고 오른 새밖열을 빼도 모든 기호가 안쪽에 있도록 하여 오른쪽 4개를 옵션화할 수 있다.

- 숫자 7, 8 자리에 있는 ()<> 괄호는 숫자와 함께 자주 사용하므로 숫자 모드에서도 입력할 수 있어야 한ㄷ. a > 7을 입력하려면 문자, 기호, 숫자 모드를 번갈아 바꿔야 하는데 >를 숫자키 바깥으로 빼면 숫자 모드에서도 입력할 수 있다. 이 자리에 들어오는 :;"'는 숫자와는 같이 입력할 확률이 낮다.

- 멀티미디어 관련 키는 모두 집안열에 모았다. media select, stop 키도 포함하여 총 8개이며 개수가 집안열과 딱 맞다.

- cut, copy, paste를 왼쪽 위에 배치하여 클립보드 액션의 편의성을 도모한다. 이 키도 어차피 Edit 키와 같이 눌러야 하니 Ctrl+C보다 별반 나을 거 없는 거 같지만 Edit 모드는 조합이 아닌 두번 누르는 방식이라 다르고 대량 작업시 Edit 락 모드에서 오른손에 마우스, 왼손으로 클립보드 동작을 반복할 수 있다.

- 윈도우 가상키로 정의되어 있는 zoom, help, select, exec, sleep 등의 키도 편집 모드에 모두 배치하여 모든 키에 편집 기능을 다 부여했다. zoom이나 sleep 키는 나름 실용성도 있다.

1행의 존재가 아무래도 마음에 걸린다. 원터치라도 홈포지션에서 두칸씩 올라가는 것은 제자리 원칙에 맞지 않고 실제로 보지 않고 누르는 것이 거의 불가능하다. 그럴 바에야 좀 구겨 넣더라도 3행으로 해결을 보는 것이 더 낫다는 생각이 들었다. 어차피 Num 모드의 왼쪽이 다 비어 있으니 꽉 채우고 1행을 없애 버려 보기로 했다. 새밖열을 도입하여 키에 여유가 생긴 것도 한 원인이다.

쿼티 키보드에 기호키는 총 21개이며 Shift까지 총 42개이다. 새밖열까지 합치면 33개여서 부족하지만 언어 영역에 세 키를 할당하고 새밖열에도 2개씩 할당할 수 있으며 공백에도 0을 넣을 수 있다.

 

기호 전용 키 6* 2 = 12.

공백키에 0 배치

Num 모드에 29개 배치

 

이러면 키 하나가 남는데 여기에는 유로 문자를 넣으면 딱 좋다. 나라별로 자국 화폐 기호를 넣을 수도 있다. 이 변화에 의해 다음 배치를 만들었다. 1행이 사라짐으로 해서 많은 부분이 바뀌었다.

 

- 3행 오른쪽 새끼키에 있던 V와 ㅋ, ㅍ을 집안열로 이동하고 이 자리에 "'기호를 배치한다. ,!.?"' 는 모든 언어에서 필요한 공통 기호여서 문자 영역에 배치하고 언어에 상관없이 고정이다.

- 한글 자모가 들어와 집안열은 한글에서도 필수이다. , ㄸ은 없애고 ㅋ, ㅍ을 넣음으로써 좌자우모의 구조를 확립했다. ㅍ이 있던 자리에 ㅑ를 배치하고 ㅑ자리에 ㅠ를 배치하여 ㅒ, ㅖ만 빼고는 모두 단독키이다.

- 오른 새밖열에는 모드 변환없이 자주 사용해야 하는 =_:;을 배치했다. @^은 좀 애매한데 빈도는 떨어지지만 (){} 괄호가 짝을 이루는 키여서 한키에 넣기 어렵고 +-는 빈도가 높아도 숫자 옆에 있는게 같이 치기 좋다.

- 숫자를 한칸 내리고 공백 자리에 0을 두어 기존 NumPad 구조와 같게 만든다. 숫자와 공백, 소수점을 같이 입력하기는 좀 번거롭지만 어쩔 수 없다.

- 왼쪽 Num 모드에 괄호와 나머지 기호를 배치한다. 다 들어가고 한 자리가 남는다.

- 편집 모드도 한칸 아래로 내리고 오른쪽 편집키와 같은 구조를 가진다. 집안열에는 멀티미디어 키를 두고 새끼열, 새밖열에 그나마 자주 쓰는 키를 배치했다. 만약 더 필요한 키가 있다면 FN이랑 조합해야 한다.

- Num 모드가 꽉 차니 Num 키가 두 개 필요해졌다. 왼손 약지로 Num\]~를 같이 누를 수 없어 자세가 흐트러진다. 그래서 Num을 양쪽에 배치해 봤는데 같은 키가 2개 있다는 것도 별로고 구분해서 쓰는게 이미 불편하다. Num도 엄지로 눌러야 나머지 키로 편하게 누를 수 있다. 엄지 영역에 남는 키가 없어 BSShift+Space로 누르기로 하고 한영을 BS 자리로 옮겼다. Edit를 안쪽으로 옮기고 더 자주 쓰는 Num을 바깥에 배치했다. 엄지를 오므리는 것보다는 펴는 것이 나머지 손가락의 자세에 유리하다.

-왼 새밖열의 Esc 이하도 한행씩 내렸다. FN은 원래 Num이 있던 자리로 이동했다. Ctrl 왼쪽에 둘까지 생각해 봤는데 이러면 Win키도 똑같이 이동해야 한다. 새밖열보다는 약지 자리가 누르기 편하다.

-펑션키는 3개씩 묶어 4그룹으로 분류했다. 윗열이 어차피 공간이 남기도 하고 4개씩 한묶음인 것보다는 그룹을 나누어 누르기 편할 거 같아서이다. 그런데 요래 놓으면 F3, F4를 교대로 누를 때 거리가 떨어져 좀 불펺지 않을까 싶기도 하다.

여기까지 배치한 결과는 다음과 같다. 46키이며 이 키만으로도 모든 것을 다 입력할 수는 있다. 다만 익숙해지는데는 시간이 좀 걸릴 거 같다.

Shift+Enter에도 별도 기능을 더 할당할 수 있다. 그리고 Esc, Tab, Caps를 완전히 왼쪽으로 빼고 기호키 3개를 더 넣을까도 생각중이다. 오른쪽 편집키와 대칭을 이루려면 왼쪽에도 키가 있어야 하고 매크로키도 배치할 수 있다. 추가한 키 3개에 편집 모드에서 Cut, Copy, Paste까지 넣을 수 있다. 숫자모드의 $자리에 .을 중복 배치하면 실수 입력도 편리해지는 이점이 있다.

21313: 46, 58 배열 조정

446키와 558키 두 배치는 아직도 고민중인데 두 가지 배치를 같이 병행 진행하기로 했다. 어차피 남는키는 안 쓰면 그만이니 하드웨어는 58키로 제작하고 소프트웨어 옵션으로 선택하면 된다. 두 배치의 차이점을 정리한 후 장단점을 비교했다. 그 전에 먼저 용어가 명확하지 않아 부분을 칭하는 용어부터 정리한다.

국제 표준에는 아래행부터 A,B,C 순으로 알파벳을 붙이고 왼쪽부터 1, 2, 3열식으로 번호를 붙여 C13 식으로 좌표를 칭하는데 매번 구조가 바뀌다 보니 이런식으로는 부분을 정확히 칭하기 어렵다. 그래서 별도로 이름을 붙여 칭한다. 이러지 않으면 과거 기록을 볼 때 어디를 칭했는지 애매한 경우가 많았다.

 

- 5행은 기호가 0행에 있고 새밖열, 언어 영역에도 있어 모드 변환없이 Shift만으로도 웬만한 기호를 다 입력할 수 있다. 숫자 모드에 숫자, 연산자만 오른쪽에 있을 뿐 왼쪽이 비어 있으며 오른쪽에 Num 모드에 연산자를 중복 정의해 놓기도 했다. 왼쪽이 비어 Num을 왼손 약지로 눌러도 불편이 없다. 4행은 왼쪽 영역에도 기호를 배치하여 잦은 기호 입력시 효율이 떨어진다. 그러나 0행의 기호도 윗열까지 올라가야 하니 이동 거리가 길며 안보고 치기는 어럽다. 또 많이 쓰는 10개 빼고는 Shift를 눌러야 하니 어차피 투터치인 건 같고 거리가 멀어 더 힘들다.

- 4행은 Num의 빈도가 높고 왼손도 기호 입력에 사용해야 하니 엄지에 배치해야 한다. 이로 인해 다른 변화가 연쇄적으로 발생한다. EditLang으로 이동하고 LangBS로 이동하고 BSShift+Space로 대체한다. Num자리는 FN이 오고 왼새밖열은 한칸씩 아래로 이동했다.

BS가 단독키가 아니라는 점은 아쉽지만 엄지쪽에 남는 키가 없어 어쩔 수 없다. 오른손 엄지를 옆으로 약간 이동하는 것보다 Shift+Space가 투터치라도 이동이 없다는 이점은 있다. 초보자는 BS를 한참동안 찾을 거 같다.

- 5행은 숫자 456이 중앙이 아니라 한칸 위로 올라가야 하지만 소수점, 공백 입력이 쉽다. 4행은 456이 중앙이라 표준 NumPad와 구조가 같지만 0Space로 내려오고 소수점은 문자 모드로 바꿔야 하는 단점이 있다. 5행은 숫자, 소수점, 공백을 같이 입력하기 편하고 4행은 오로지 숫자 입력만 편하다.

- 5행은 편집키가 홈 포지션이라 이동이 신속한 장점이 있지만 Del, Home, PgUp0행에 있어 이동 거리가 길다. Help, Sleep, Zoom 등 잘 안쓰는 키까지 다 넣을 수 있고 Cut, Copy, Paste도 배치할 수 있어 활용성은 높다. 4행은 좌우 이동을 위해 한칸 내려와야 하지만 Del키 입력이 쉽다. 딱 필요한 편집키만 넣을 수 있지만 딱히 부족하지는 않다. 혹시 더 필요한 키가 있으면 FN 조합으로 넣으면 된다.

- 한글, 영문 자모 배치에도 불가피하게 변화가 발생한다. 5행은 언어 영역에 ,. 두 개의 키만 기호로 할당하지만 4행은 자주 쓰는 "까지 포함하여 하나 더 늘렸다. 이 자리에 있던 V와 ㅋ이 집안열로 이동하고 ㅍ도 같이 이동하여 좌자우모 원칙을 지켰다. ㅋ 자리에 ㅑ를 넣어 복모음은 ㅒㅖ 빼고 다 키를 할당했지만 경음은 ㅆ만 키를 할당하였다.

 

두 배치를 같은 페이지에 그려 보았다. 이왕 그리는 김에 각 키의 문자 크기, 정렬 상태도 편집하기 쉽도록 바꾸고 엄지키는 다시 그렸다.

두 배치를 같이 그려 놓고 장단점을 따져 보니 현재 느낌으로는 4행이 더 나은 거 같다. 5행은 키가 12개나 더 많지만 편의성이 월등히 높은 것도 아니고 남는키, 중복키까지 있어 어색하다.

4행이 오히려 숫자 홈포지션이 맞고 편집키 입력도 쉽고 좌자우모의 원칙도 잘 지킨다. 게다가 4행은 손가락당 아래위, 좌우만 있어 제자리 원칙에 충실하며 키를 세개씩 묶어 컴팩트한 배열을 만들 수 있다. 좌우가 구분되어 있으니 얼마든지 띄워도 되고 손목 각도에 맞춰 기울여도 된다. 10도씩 기울였다.

이 그림 그리는데 1시간 걸렸다. 이게 최종적으로 내가 만들고 싶은 키보드의 모양이며 최초 특허에도 포함되었다. 다만 집안, 새밖열이 추가되어 폭이 길어졌고 기호 입력의 번거로움과 BS가 단독키가 아닌 것이 아쉽다.

키 개수가 적으면 불편함이 따르고 연습이 필요한 것은 어쩔 수 없는 일이다. 다만 그 불편함이 익숙해질 수 있는 수준인가와 익숙해졌을 때의 이점이 불편함을 능가하는가가 중요하다. 너무 완벽하려고 애쓰지 말고 최소한 쿼티보다는 좋게 만들되 미래의 변화를 수용할 수 있을 정도면 된다.

 

-------------------------

 

이왕 4행을 확정지으려면 다국어까지 고려하여 키 개수가 조금 더 넉넉해야 할 거 같다. 국제 키보드를 좀 연구해서 정리했는데 일본어와 중국어는 로마자를 변환하는 식이라 그다지 고려할 필요는 없을 거 같다. 독일어와 프랑스 키보드를 연구해 보니 참고할만한 내용이 많다.

독일어는 알파벳에 비해 에스체트, 움라우트 3개까지 4글자가 더 많고 프랑스어는 ÀÈÉÊÆÙ 이 정도 문자가 더 있다. 스페인이나 다른 나라도 비슷할 거 같다. 이 문자들 때문에 알파벳 배치가 조금 달라지고 기호들도 이동한 예가 많다.

또 쿼티 키보드에 있지 않은 0x80~0x256 사이의 1바이트 문자를 많이 배치해 놓았는데 같이 배치해 두면 국제화가 쉽고 잘 안 쓰는 문자들까지 대중화할 수 있을 거 같다. 유럽 키보드는 다음 기호가 더 있다. 이중 일부는 쿼티의 기호 자리를 차지할 정도로 많이 쓰이는 모양이다.

 

문자

유니코드

설명

§

A7

Section. 유럽 키보드에는 대부분 포함되어 있다.

µ

B5

Micro. 프랑스, 독일 키보드에 포함

±×÷

B1 D7 F7

수학기호

¿¡

BF A1

물음표, 느낌표 반대 문자

·

B7

중앙점

°

B0

온도

²³

B2 B3

제곱, 세제곱

«»

AB BB

프랑스어 9, 0Shift 자리에 있음.

 

왼새밖열에 키 3개를 더 두거나 5행이면 이 기호들도 다 포함할 수 있고 타 알파벳을 위한 여분 영역까지 확보할 수 있다. 당장 안쓰더라도 키를 미리 확보해 놓으면 다국어 키보드를 만들기 더 수월해진다.

일단 4행에 새밖열을 추가하여 배치해 봤다. 키는 3개 늘어 났지만 배치할 수 있는 기호는 Num 모드의 기호까지 12개이며 편집키 3개를 더 쓸 수 있다. 새밖열의 ETC키가 왼쪽으로 밀려남으로써 또 많은 변화가 발생한다.

- 기호 12개를 추가했다. 쿼티 키보드에는 없는 문자들이며 유럽을 위해 추가한 것이다. 왼새밖열은 모드 변환없이 누를 수 있어 (){}를 두는 것이 어떨까 했는데 제자리에서 Num+집게,중지로 누르는 것보다 새끼 손가락으로 바깥을 누르는게 딱히 더 편하지도 않다. 그나마 Shift없이 누르는 3키만 그렇지 위에 있는 기호는 Shift와 함께 새밖열은 눌러야 하니 오히려 더 불편하다. 결국 추가한 3키는 그냥 유럽용일 뿐 영문, 한글에는 별다른 이점이 없는 셈이다.

- 원래 새로 추가한 새밖열에 Cut, Copy, Paste 편집키를 넣을려고 했는데 그보다는 오른손 집안열에 두는게 더 좋을 거 같다. 편집 상태에서 이동 및 선택 후에 바로 잘라내고 붙여 넣을 수 있다. 멀티미디어 키와 자리를 바꾸었다.

- ETC가 밖으로 밀려나면서 1열인 건 모양이 좋지 않다. 오른쪽 편집키가 4행인 것도 마음에 안든다. 그래서 PrtScIns를 데려왔다. FN키는 문자 입력중에 잘 쓰지 않으니 왼쪽 구석에 쳐박아 두었다. 오른쪽 편집키 9개는 원래 엄지행에 맞추었는데 위로 한칸씩 올려 편집영역키와 위치까지 완전히 일치시켰다.

-FN키가 물러난 자리를 Win키를 채워 넣었다. 원래 왼쪽 약지 자리라 딱 어울린다. Win 키가 물러난 자리에 Enter키를 배치하고 Enter 자리에 다시 BS를 도입했다. Enter는 입력중에 자주 누르는게 아니라 입력 완료 후에 가끔 누르므로 엄지가 아닌 약지 영역에 두어도 괜찮을 거 같다. 원래 새끼 2칸 건너 자리보다 약지 2칸 아래가 더 낫다. BS가 다시 단독키가 되어 Shift+Space를 칠 필요가 없어졌다.

 

이상 52 + 12 + 9 = 73키이다. 가로폭이 무려 17키인데다 중앙 빈 영역까지 더하면 104키 풀배열의 가로 22키랑 거의 같은 폭이 되고 만다. 오른손 편집키를 빼야 겨우 텐키레스폭 정도라 너무 거대해졌다. 이럴 바에야 차라리 5행으로 구성하여 잘 안쓰는 키를 몰아 넣는게 더 좋지 않을까 싶다.

애초의 의도는 손가락이 닿는 28키로만 만들려고 했는데 이것 저것 고려하다 보니 자꾸 비대해지고 있다. 한번 시도는 해 봤지만 다국어까지 미리 고려하는 건 좀 오바가 아닌가 싶다. 그래도 이런 저런 시도를 해 볼 때마다 하나씩 아이디어가 추가되고 있어 최종적인 조합에 쓸 재료가 많아지는데 의미를 둘 수 있다.

 

---------------------------------

 

이왕 만드는김에 0행까지 같이 추가해 보기로 했다. 키를 이렇게 늘려도 () {}는 여전히 모드를 바꿔야 하니 불편하고 0행의 집게, 중지, 약지 영역만 잘 활용해도 더 많은 문자를 수용할 수 있다. 대신 집안열에는 문자를 할당하지 않고 새밖열에는 문자가 아닌 Esc, PrtSc키를 놓기로 한다.

우밖의 키 3개가 사라졌고 0행 집안열은 미사용이어서 총 77키이다. 미사용키는 다른 나라를 위해 자리만 있고 실제키는 없는데 옵션키를 다 생략하면 56키가 된다.

 

-0행의 괄호와 :;Shift없이 언제든지 입력할 수 있되 한칸 더 위로 올라가야 하는 불편함이 있다. 제자리 원칙에는 부합하지 않는다.

-기호를 전부 오른쪽으로 옮기고 왼쪽에는 유럽 기호만으로 채웠다. 문자 다양성은 확보했는데 한글 입장에서는 좀 쓸데없는 짓 같기도 하다.

-추가 문자와 기호를 위한 많은 여분이 있다. 0행과 새밖열은 기호가 할당되어 있지 않아 더 추가할 여지가 많다. , 소수점 입력을 위해 .3행우집열에 중복해 두었다.

-0, 오른 새밖열이 비어 있고 ㅋ자리도 비어 있어 다른 언어의 추가 문자를 12개까지 더 수용할 수 있다. 미용키까지 동원하면 14개라 비교적 충분하다.

 

모양이 나쁘진 않은데 엄지행이 좀 애매해졌다. 한글, 영문은 Num이 엄지에 굳이 있을 필요가 없고 Enter가 엄지 바깥인게 영 맘에 안든다. 다국어를 위한 확장성을 위해 일단 그냥 두되 프랑스, 독일의 경우 Lang키가 굳이 필요치 않아 이 자리를 Num으로 바꿔 써도 될 거 같다.

이번주는 여기까지 작업하고 마무리한다. 이것 저것 시도할수록 더 명확해지는 것이 아니라 갈수록 복잡해져 가는 느낌이다. 다국어만 아니라면 현재로서는 46키 배치가 가장 무난하지 않나 생각된다. 일주일 내도록 좀 더 고민해 봐야겠다.

21315: 영문, 엄지행 조정

어제 작업 끝내 놓고 문득 쿼티와도 호환되어야 한다는 생각이 들었다. 키 개수를 결정하기 위한 배치 작업중인데 쿼티를 빼 놓으면 차후 대중화되기 어렵다. 아무리 잘 만든 물건도 이전 세대에서 이행하는데 시간이 필요하므로 호환성을 완전히 배제하면 성공하기 어렵다.

키 개수에 쿼리 호환 가능성도 고려해야 하며 결국 5행으로 키보드를 만들 수밖에 없다. 56키는 안되며 결국 집안열 0행의 키도 있어야 함을 알 수 있다. 58키 구조상 알파벳은 다 들어가지만 새밖열의 =]\` 4개키 8개 문자를 배치할 키가 없고 왼새밖3행의 키 하나가 남는다. 남는 키는 Num 모드에 골고루 넣는 수밖에 없다. 이래서 나온 배치는 다음과 같다.

- 알파벳, 한글, 숫자, .,/;' 구두점까지는 쿼티와 완전히 똑같이 배치한다.

- 빠진키 4개를 문장 영역에 놓아야 하는데 새밖열의 ]키는 그 오른쪽의 ]키를 놓을 자리가 없어 찢어 놓을 수 없다. 부득이하게 이 자리에 바로 위의 =키를 갖다 놓고 우새밖3`를 놓는다. 남은 []{}\|는 왼쪽 집게, 중지의 Num 영역에 배치한다.

- Num 모드의 오른쪽은 58키 배치와 같이 유지하여 .+-는 똑같이 두고 /는 중복하고 *는 집안2행에 두어 숫자와 연산자를 모아 둔다. Num키의 기능은 58키와 거의 같도록 하여 호환 모드에서도 실습할 수 있도록 한다.

- Edit 모드는 58키와 같다. 엄지행도 별도로 수정하지 않는다. 편집 영역의 PgUp, PgDn, Home, End는 쿼티가 아닌 58키 형식을 유지한다. 쿼티 호환이더라도 제자리 자판 형식이 우수하다는 것을 느낄 수 있도록 하자는 의도이다.

 

이 정도 배치면 쿼티 사용자가 점진적으로 이 자판에 익숙해지는데 충분하지 않을까 생각된다. 기호 몇 개 바뀐 거 외에 문장을 칠 때는 똑같이 쓸 수 있고 엄지열의 여러 이점도 얻을 수 있다.

 

-----------------------

 

쿼티 호환 자판을 만들다 보니 유독 ;이 눈에 들어오기 시작했다. 쿼티는 문자 영역에 4개의 기호키를 할당하고 ,./;Shfit 영역에 <>?:를 배치해 놓았다. 그리고 새밖열에 '"도 단독키로 되어 있다.

문자 영역의 키 4개가 기호로 할당되어 있는 이유는 30개키중 알파벳 26자를 배치하고 남은 수만큼이며 모든 나라의 알파벳 숫자와 맞지는 않다. 독일어는 ;' 자리에 움라우트가 있고 프랑스어는 ;자리에 M이 있으며 기호가 다른 곳으로 밀려나 있다.

내 자판은 숫자가 빠져 여유분이 다소 있어 기호를 좀 더 박아 넣어도 될 거 같다. 이중 ;은 개발자에게는 마침표와 같아 사용 빈도가 무척 높으며 자리가 바뀌면 적응하기 쉽지 않은 문자이다. 58키는 ;0행우집게에 있고 46키는 1행우새밖에 있어 무척 비효율적이다.

이 문자를 쿼티와 같은 위치에 놓는 것이 좋다는 생각이 들었다. 빈도상으로는 문자에 비해 4개의 기호키를 할당하는게 꼭 합당하지는 않지만 적어도 한글, 영문에는 나쁘지 않은 선택이다. 쿼티의 ; 자리에는 알파벳 B와 한글 ㅍ이 있고 46키는 ㅑ가 중복되어 있어 하나씩만 옮기면 된다.

5행은 어제 만들었던 77키의 배치를 가져오되 0행의 생략키까지 다 넣었다. 4행은 왼새밖열을 추가하기 전의 46키를 가져왔다. 둘 다 엄지행은 일단 통일시켜야 하므로 Enter를 약지로 뺀걸 일단 적용했다. 물론 엄지행은 이후에도 얼마든지 바뀔 수 있다. 46키는 FN을 놓을 곳이 없어 Caps와 키를 중복시켰다. 단독으로 짧게 누르면 Caps이고 다른 키와 조합하면 FN으로 동작하면 된다. 먼저 46키의 배치부터 수정한다. 단 한글자 이동이라도 꽤 많은 변화가 발생한다.

- B를 단순히 남는 자리인 1행왼집안으로 보내서는 안된다. 사실 이전의 V도 그런식으로 쫒겨나 대충 자리 잡은 것인데 완전 잘못 잡은 것이다. B의 제자리를 찾기 위해 빈도를 다시 참조해야 한다.

 

B : 1.5

V : 0.97

K : 0.77

J, X : 0.15

Q : 0.09

Z : 0.07

 

B가 오히려 문자영역의 K보다 빈도가 더 높다. 그래서 BK 자리로 보내고 나머지 여섯자를 집안열에 빈도순으로 배치한다. 집안열 알파벳의 빈도는 2% 정도인데 분당 500타 기준으로 계산해 보면 10회 정도 손가락이 들어온다. 이중 자리가 그나마 좋은 VK를 빼면 나머지 4키의 빈도는 0.5%이며 1분에 2.5번 정도 쳐야 한다.

참고로 쿼티 키보드의 집안열에는 TYGHBN이 있으며 전체 합산 빈도가 27.2%에 속해 4번에 한번꼴로 집안열로 들어오는 셈이다. T의 점유율이 9%이며 무려 2위인데 너무 안 좋은 자리에 있다. 좀 치기 쉬운 GH를 빼도 20% 가까이 된다. 그만큼 쿼티 배치가 개판이라는 뜻이다. 0.5% 확률이면 쿼티에 비해 40배나 낮으므로 괜찮은 확률이다.

- 한글 ㅑ는 중복된 것이므로 그냥 지워 버리면 된다. 그러나 빈도상 더 낮은 ㅠ가 있는데 ㅑ가 없는건 말이 안되니 ㅠ자리에 ㅑ를 가져다 놓는다. 어차피 중복이라 어디다 갖다 놓으나 중요치 않다.

-비운 자리에 ;:을 가져온다. 그리고 새끼3행에 있던 " '를 그 옆에 배치한다. 쿼티와 가급적 기호 배치를 맞추되 큰 따옴표를 더 자주 쓰니 ' "가 아닌 반대로 했다. 이 둘은 빈도 조사해 보고 바꿀 수도 있다. " '자리에 있던 =_가 새끼 3행으로 오고 ;:가 있던 자리에는 %$를 배치한다. 이로써 모든 기호가 다 들었다.

-Num 모드에는 가감승제 연산자 4개만 들어 있다. 빈도는 높지만 숫자와 성격이 비슷해 여기 넣었다. 그리고 58키와 마찬가지로 소수점 .을 집안열 3행에 중복 배치했다.

 

여기까지 작업해 놓고 보니 알파벳이 빈자리없이 가득 채우고 있어 완성도가 더 높은 거 같다. 기호도 사칙 연산자 빼고 많이 쓰는 건 다 단독키로 줘서 큰 부족함이 없다. 다음은 58키도 비슷한 방식으로 변경한다.

- 영문, 한글은 48키와 같다.

- ;:을 빈 자리에 넣고 그 옆에 " ', 아래에 =_를 배치하여 46키와 일치시킨다.

- 가감승제 연산자와 . 중복도 46키와 같다.

- 0행에 괄호 4개를 높고 Shift 자리에 기호를 배치한다.

- 우새밖열은 3행만 ~`가 있고 1행은 비워 둔다. 이 키도 여유분인데 노는 키가 있으니 좀 이상하다. 바로 위의 PrtSc도 사실상 거의 여유분이나 마찬가지이다.

 

------------------

 

0~3행까지 문자, 기호 영역은 웬만큼 큰 틀은 잡은 거 같다. 그러나 엄지열은 좀 더 조정이 필요하다. 엄지당 3개씩 딱 6개가 좋은데 Enter가 엄지 바깥으로 밀려난 것이 못내 아쉽다.

현재 구조상 Enter는 오른손 약지 자리인데 두 칸 내려와 누르기 쉽지 않고 자세가 틀어진다. 현재 쿼티 키보드 구조로는 공백 오른쪽의 Alt키 자리인데 오른손을 들어 엄지로 누르는 사람도 많다.

이 구조를 그대로 쓰되 Shift+SpaceEnter로 중복해 놓으면 자세 유지한 채로 개행은 가능하다. 상태를 바꾸는게 아니라 즉시 입력되는 키여서 나쁘지는 않은 거 같다. 두 개 같이 누르는게 번거롭다면 Shift에 고정키 옵션을 주면 된다. 단독으로 누를 때는 Enter키를 치고 문장 입력중에는 Shift + Space를 누른다.

Enter를 꼭 엄지행으로 가져오려면 EditNum 중 하나가 밀려나야 한다. 둘 다 양쪽으로 조합하기 때문에 양쪽 다 있어야 하며 양손 중지로 누른다. 중지 두 칸 아래도 누르기 그리 어려운 곳은 아니나 자세가 약간 틀어질 거 같다.

Enter 자리에는 FN을 배치했다. 그래야 Num이 양손 모두 중지에 걸리고 좌우 대칭이 맞다. 이렇게 되면 엄지행이 번잡해지며 엄지와 중지의 동선이 약간 겹쳐 손가락끼리 꼬인다. 좌우를 좀 더 벌려 여백을 만들어 줘야 하며 좁히기는 어려워진다.

입력중에 BS, Enter를 치기는 쉽지만 숫자, 편집이 불편해지는 건 어쩔 수 없다. 두 키 모두 Hold 기능이 있어 중지를 누른 채로 있기 어렵다. 이왕 키를 늘린다면 원래 배치에서 Ctrl, Alt를 양쪽에 두면 어떨까? 쿼티키가 그렇게 되어 있으니 이것도 한번쯤 고려는 해 볼만 하다.

딱 봐도 좀 아닌 거 같다. 많이 쓰는 조합키라도 자리가 정해져 있으면 되는거지 굳이 2개씩 둘 필요는 없다. Ctrl은 양쪽을 다 쓰는 경우가 있지만 Alt는 그렇지 않고 한글키로 용도를 바꿔 쓰는 경우가 많다. , 굳이 양쪽에 있을 필요는 없다. Ctrl을 양쪽에 두는건 좀 생각해볼만 하다.

현실적인 대안은 BSEnter를 교체하는 것이다. 이 방법은 최초 4행 배열을 만들 때 이미 시도해 봤던 것이다. 이 둘의 빈도는 오타가 얼마나 발생하느냐에 따라 달라지는데 Enter는 늘상 눌러야 하지만 BS는 정확히만 치면 딱히 입력할 필요가 없다.

이 구조에서 Shift + SpaceBS로 중복 정의하면 자세가 틀어지지 않으며 논리상으로도 합당하고 직관적이다. 다만 오타가 많다면 좀 불편해지는데 제자리 자판은 익숙해지면 오타가 많지는 않아 괜찮은 선택인 거 같다.

키 하나를 입력하는데 두 가지 이상의 방법이 존재한다는 것은 일관성이 없다는 점에서 바람직하지 않다. BS를 단독으로 누르는 빈도가 확 줄어 든다면 아예 BS를 빼 버리고 Ctrl을 양쪽에 두는 것도 괜찮아 보인다.

BS가 단독키가 아니라 초보자가 좀 어리둥절해할 수 있지만 Ctrl에 대한 접근성이 좋아지고 쿼티와의 호환성도 향상된다. BS는 어차피 입력중에 누르지 단독으로 누를 일이 별로 없다. Enter는 단독으로도 누르는 경우가 많다.

Shift + Enter에도 뭔가 기능을 부여할 수 있을 거 같다. 그러나 엑셀이나 카톡 같은 프로그램은 이 조합에 다른 의미를 부여하기 때문에 키보드가 미리 기능을 정의해 버리는 것은 바람직하지 않다.

오른쪽 편집키가 따로 없는 상태에서 Ctrl+Alt+Del을 누르려면 Edit까지 4개의 키를 눌러야 하는데 그나마 CtrlAlt를 모아 놓으면 좀 쉬워지는 이점도 있다.

21320- 인체공학 키보드 분석

태양 아래에 새로운 것은 없다. 내가 뭘 열심히 궁리하고 만들고 있다면 이미 그런 생각을 한 사람이 있을 것이다. 실패했든 성공했든 참고할 것이 있고 실패했다면 원인을 파악하여 해소해야 한다. 중고 장터를 뒤지다 보니 인체공학 키보드는 많이 있다.

X-bows라는 키보드이다. 킥스타터에서 250$에 펀딩했으며 현재 옥션 해외구매가로 18만원 정도 한다. 활처럼 휜 키보드라는 뜻이다. 중고로 64000원에 구매해서 아직 써 보기 전이다.

좌우 기울임 각도가 내가 생각한 것과 같고 중앙이 비어 있다. 손가락 길이에 따ㄹ라 새끼쪽이 아래로 내려가 있다. CtrlShift가 엄지에 달려 있는 것도 내 생각과 같은데 세쌍이 중복되어 있다. EnterBS도 중앙이기는 한데 집게 손가락으로 누르도록 되어 있는 거 같다. 기호키의 배치는 쿼티의 한계를 벗어나지 못했다.

이런 키보드를 이미 만들어 팔고 있다는 것은 다들 생각하는게 비슷하다는 뜻이다. 그러나 사용기를 보면 적응에 실패한 사람들이 많다. 중고로 나온 매물도 다 2차 구매자이며 몇 번이나 주인이 바뀌었는지 알 수 없을 정도다. 키피치가 너무 벌어져 있고 Home, End키가 보이지 않는다.

ZSA에서 만든 Moonlander 키보드이다. 관세 포함 대략 45만원 정도 하며 중고 매물도 35만원은 넘는다. 기계식 핫스왑이며 좌우가 유선으로 연결된다. 엄지쪽은 손 크기에 따라 육각렌치로 각도를 조절할 수 있고 팜레스트와 뒤쪽 레버로 각도 조절이 가능하다.

펑션키가 없고 PgUp, PgDn 등이 편집키가 안 보인다. 커서 이동키는 상하 오른쪽에, 좌우는 왼쪽에 나누어져 있다. 키배치는 평범한 쿼티이되 기호가 다 포함되어 있지는 않다. 엄지쪽 키는 기능이 정해져 있지 않은 사용자 정의형인 거 같다. -로 표시된 키도 다 사용자 정의형이다. 모든 키의 배치를 소프트웨어로 바꿀 수 있다.

하드웨어는 독특하고 체형에 따라 커스터마이징 가능한 거 같은데 키 배치나 소프트웨어는 많이 신경쓰지 못한 거 같다. RGB 조명이나 파우치 등 쓰잘데기없는 기능도 포함되어 있다.

이 키보드의 전세대로 Ergodox EZ가 있었다. 엄지쪽에 훨씬 더 많은 키가 배치되어 있다. 좌우 분리형인데 4극 오디오 케이블을 사용한다.

키 맵핑은 소프트웨어로 지정하는데 초기 지정은 다음과 같다. 레이어는 일종의 모드로 특정 키를 누르면 배치가 바뀌는 것이다. 부족한 키를 보충하는 수단이다. 엄지를 일반 편집키나 조합키로 쓸뿐 모드를 변경하지는 않는 모양이다.

이 키보드를 연구하다가 더 재미있는 것을 발견했는데 https://shurs-island.tistory.com/13 사이트에 직접 만드는 게시물이 있다. 중국에서 부품 조달하고 3D 프린터로 케이스를 만들어 조립하는 식이다. 시제품을 만들 때 상당히 많은 참고가 될 거 같다.

키네시스(Kinesis)도 인체공학 키보드를 많이 만드는 회사다. 일단 좌우 분리형인 FreeStyle Pro가 있다. 중간의 선이 좀 걸리적거릴 거 같고 본체와도 유선으로 연결하기 때문에 걸리적거릴 거 같다.

이런건 예전에도 많이 있었고 분리형일 뿐 키 배치는 그냥 콤팩트일 뿐이다. 다만 왼쪽의 키 10개가 특이한데 Cut, Copy, Paste, Select All이 단일키로 제공된다. Ctrl 조합키를 대신 입력해주는 방식인 거 같다. 다음은 Advantage2이다. 가격은 대략 400$이고 50만원 정도가 들어간다.

인체공학의 끝판왕쯤 되는데 일단 무척 못생겼다. 각 엄지에 키를 6개씩이나 할당한 것은 좀 과도해 보인다. 문자키는 수직 배열되어 있고 새끼 손가락은 약간 아래쪽이다. 방향키는 좌우에 나누어져 있다. 손을 옮기지 않고 제자리에서 거의 모든 것이 가능하다. 아무리 그래도 펑션키를 저리 박아 놓으면 디버깅할 때 무척 불편할 거 같다.

사용기를 읽어 보니 최소 1~한달은 고생 좀 해야 한다는데 익숙해지면 무척 편하다고 한다. 다른 사람이 내 키보드를 못 쓰는 장점이 있는 반면 나도 다른 사람의 키보드를 쓸 수 없다. 좌우 나누어진 키보드는 한글의 ㅠ 입력이 어렵다고 한다. 페달을 추가하면 Ctrl, Alt를 맵핑해서 쓸 수 있다고 한다.

근데 이 페달을 79$에 팔아 먹고 있다. 이거 아니라도 대체품은 얼마든지 있을 거 같다. 페달은 별도의 제품이지 키보드의 부속품이 아니다.

남의 키보드 연구해 보니 만들어야 할 키보드의 형태가 대충 잡힌다. 처음부터 너무 거창하게 만들지 말고 컴팩트한 보편성을 가지는 것이 좋다.

 

- 키 개수 : 인체공학은 키가 많지 않고 다 맵핑해서 쓰는 형식이다. 최소 개수로 디자인하고 추가 옵션키를 두면 된다.

- 좌우 분리 : 무선 아닌 바에야 안하는게 좋다. 책상이 번잡스러워지고 선이 걸리적거리며 키보드가 덜썩거려 불편하다. 하나로 만들되 간격을 적당히 띄운다.

- 기울이기 : 좌우를 약간 벌리기만 하면 될뿐 굳이 기울일 필요 없다. 너무 튀는 것도 거부감을 준다. 차후 개선 형태로 시도해볼 수는 있다.

- 손가락별 높이 : 오랫동안 손을 오므리고 키보드를 사용해 왔기 때문에 새끼 손가락만 아래로 내릴 필요 없다. 어차피 손가락을 쭉 뻗은 채로 타이핑하지 않는다.

 

현재 기획상으로는 대충 이 정도가 되지 않을까 싶다. 복사해서 붙여 넣었더니 색깔이 왜 저따구로 나오는지 모르겠네.

이건 최종 제품 모습이고 당장 이렇게 제작하기는 어렵다. 5행이 다 필요하니 특허만 이렇게 내 놓고 개별 스위치로 만들어야겠다.

-------------------------------

키 개수나 모양은 거의 결정했다. 46, 58키에서 큰 변화는 없을 거 같고 58키는 쿼티 호환을 위해 유지할 뿐 거의 46키로 마음이 굳었다. 다만 언어에 따라 ETC 자리에 문자키 3개를 추가하거나 엄지행에 조합키를 좀 더 둘 수 있는 정도이다.

다음은 알파벳과 한글의 문자 배치를 결정해야 한다. 문자 영역에 기호키 4개를 걸정했으니 이제 26개 키에 알파벳을 효율적으로 배치해야 한다. 예전에 기초 연구와 고민을 충분히 하여 결정했지만 주관적인 배치였다.

더 정밀한 점검과 다양한 배치를 테스트해 보기 위해 컴퓨터를 이용한 점수화와 객관화가 필요하다. 미처 고려하지 못했던 점을 살펴볼 수 있다. 여러 배치에 대해 점수를 계산하고 배치의 근거를 분명히 제시해야 이견과 비판에 대해 대응할 수 있다.

먼저 조합의 수를 생각해 보자. 26개의 키를 26개의 위치에 순서대로 배치하는 문제이므로 순열이다. 26개를 전부 배치해야 하므로 경우의 수는 26!이 된다. 이 큰 수를 어떻게 구할까 생각하다가 그냥 오라클 쿼리문으로 단순 계산해 봤다.

 

select 26*25*24*23*22*21*20*19*18*17*16*15*14*13*12*11*10*9*8*7*6*5*4*3*2*1 from dual;

403291461126605635584000000

 

403 2914 6112 6605 6355 84000000

 

27자리이며 1026승으로 우연이겠지만 지수와 알파벳 개수가 일치한다. 403= 403조조이다. 억조경해자양구간정재극(10^48)까지가 한자리 단위이며 이후는 항하사, 아승기, 나유타, 불가사의, 무량대수, (10^72)이다. 항하사는 겐지즈강의 모래알수라는데 사실 그보다 훨씬 더 큰 숫자이다. 겁은 시간의 단위이며 한변이 16Km인 바위를 100년에 한번씩 흰천으로 닦아서 닳아 없어질 때까지의 시간으로 정의되어 있다.

이 정도 조합이면 슈퍼 컴퓨터로 초당 1억개의 배치 상태를 점검해도 400억년이 걸리고 그 결과를 다 저장할 수 있는 스토리지도 존재하지 않는다. 일정한 기준에 따라 경우의 수를 대폭 줄이지 않는한 컴퓨터로도 배열의 효율성을 점검할 수 없다.

 

- JXQZ 네 개는 다 더해도 빈도 0.5%밖에 안되며 집안열에 임의 배치할 것이므로 제외한다. 경우의 수는 22!11해이며 그 앞 순위인 K, V까지 빼면 240경으로 루프 돌릴만 하다. 그러나 K, V는 분담률을 고려하면 다른 위치로 갈 수도 있지만 일단 포함시켰다. 240경도 PC로는 어림도 없다.

- 빈도별로 그룹을 나누어 그룹 안에서만 배치한다. 예를 들어 5, 5, 6, 6으로 나누어 그룹내에서만 바꿔가며 배치하면 경우의 수는 5!*5!*6!*6! = 74억번이며 이 정도면 PC로도 루프를 돌릴 수 있다. 그러나 그룹 외부로의 이동이 금지되어 변화의 정도가 약하며 모든 경우를 테스트해볼 수 없다.

- 글자와 위치의 우선순위를 정해 두고 각 글자를 우선순위 앞뒤로 일정칸 이상만 움직여 본다. 예를 들어 빈도 순위 3A는 앞뒤로 두 칸씩만 이동하여 1~5위 사이의 자리에만 배치한다. 앞이나 뒤가 없는 글자는 이동 가능한 쪽으로만 이동한다. 예를 들어 빈도 1위인 E는 더 낮은 쪽이 없으니 1~5위 사이로 이동한다.

-er, th, eh, in 등 연이어 나올 확률이 높은 철자에 대해 연타가 발생하는 배치는 무시한다. 한두개는 연타가 발생할 수 있어 너무 가혹한 조건일 수 있지만 황당한 배치를 걸러내는 효과가 있어 더 연구해 봐야 한다. 알파벳은 좌자우모가 아니지만 모음끼리 너무 몰려 있는 것도 바람직하지 않다.

 

다음은 각 위치에 대한 우선순위와 부담도를 정의한다. 우선순위는 더 좋은 자리를 의미하며 좋은 자리부터 순서대로 번호를 매긴 일련 번호이다. 부담도는 이 키를 누르는데 필요한 에너지를 의미한다.

기호가 이미 자리를 차지한 영역, 집안열 아래위의 통계 대상이 아닌 키는 번호를 매기지 않았다. 그러나 기호도 빈도와 좌우손의 연타율 등을 고려해야 하므로 부담도는 정의하고 통계에 포함시킨다.

 

-> 폐기됨. 47일자로 대체함.

 

그림에는 없지만 Space, Enter, Shift도 빈도를 같이 계산해야 한다. 숫자나 구두점 이외의 기호는 다른 영역이며 어차피 같은 모드가 아니므로 고려할 필요 없다.

이 순위의 근거는 다음과 같되 정확한 기준이나 데이터가 없어 다소 주관적이고 임의적이다. 차후 얼마든지 더 정밀하게 조정할 수 있다.

 

- 좌우손의 대응 위치는 순위와 부담도를 같다고 평가하되 오른손이 마우스를 담당하므로 왼손에 더 우선 부여했다. 0, 12, 3은 우선순위가 같다.

- 같은 손가락 아래위도 같은 위치이되 위쪽으로 뻗는 것이 아래로 구부리는 것보다 약간 더 쉽다고 생각하여 낮은 우선순위를 부여했다.

- 2행 자리가 가장 좋다. 다음은 집게, 중지, 약지의 위, 아래순이다. 집게 안쪽과 새끼 위아래는 애매한데 집게 안쪽이 더 접근성이 좋다.

- 부담도는 집게 홈포지션을 10으로 잡고 그 위, 아래를 20으로 정의했다. 손가락을 들어 자리를 옮긴 후 누르고 다시 원래 자리로 돌아오는데 2배의 힘이 드는 것으로 평가했다. 나머지는 이 두 키에 대한 상대적인 부담도로 평가한다.

 

우선순위에 따라 키를 이리 저리 배치해 보고 문장을 입력해 보면서 부담도를 계산하면 손에 가는 부담의 총량을 계산할 수 있다. 키 배열을 순회하며 다음 기준으로 점수를 매겨 평가한다.

 

- 부담도의 총합은 낮을수록 좋다.

- 좌우 분담율이 절반 또는 오른손 마우스를 고려하여 왼손이 약간 높은 정도

- 손가락별 분담율이 힘에 맞게 분산되어야 한다.

- 한손가락 연타율이 낮아야 한다.

- 한손 연타율이 낮아야 한다.

 

기준은 마련했는데 경우의 수가 너무 많다 보니 순열에 대해 식별자를 부여하고 순서대로 순회하는 마땅한 방법이 얼른 떠오르지 않는다. 다음 순열을 탐색하는 알고리즘만 해도 그리 간단하지 않고 경우의 수가 얼마나 되는지조차도 파악하기 어렵다.

그래서 일단 프로젝트부터 만들어 돌려 보기로 한다. 0~21번까지의 각 키 위치에 문자를 하나씩 배열한 것을 배치의 식별자로 정의한다. 주로 소문자로 입력하므로 알파벳은 소문자로 기록하고 대문자가 나타나면 Shift 조합으로 정의한다. 가장 먼저 점검해 봐야 할 배치는 다음 네 가지이다.

 

알파벳순 : nqmrlskdgwzchvbiuopajtefxy

쿼티 : fjdkslaruvmeicwoxghqpztybn

1차 배치 : etonislfdumarghcpkvwybzqxj

빈도순 배치 : etaonshrdlicumfgwypbvkjxqz

 

알파벳순은 좌에서 우로, 위에서 아래로 abcd...를 적어 놓고 대응 순위별 알파벳을 적으면 된다. 쿼티는 쿼티 키보드로 우선순위별로 키를 눌러서 나온 배치이다. 둘 다 집안열 아래위까지 포함해서 26자를 다 기록했다. 점수는 아마 거의 개판 수준으로 나오지 않을까 예상되는데 그나마 쿼티가 알파벳순보다는 나을 거 같다.

1차 배치는 현재 배치해 놓은 알파벳 배치를 순위별로 나열한 것이며 기호 자리 확정 후의 배치이다. 비교를 위해 집안열 위아래 4개도 상수지만 일단 배치해 두었다. 통계는 내지 않더라도 상수로 박아 넣기는 해야 한다. 그런데 이 위치는 위쪽이 더 접근성이 좋다면 xjzq로 바꾸어야 하는데 다음에 조정해 보기로 하자. 빈도순 배치는 자주 나오는 글자를 단순히 좋은 자리에 배치한 것인데 이러면 분담율이 그다지 좋지 않을 것으로 예상된다.

순열 순회 방법이 그다지 간단하지 않을 거 같은데 이건 별도로 연구해 보기로 하고 일단 덮어놓고 코딩부터 해 보기로 했다. 3개의 레이아웃에 대해 점수를 내는 방법 먼저 확보하고 다른 레이아웃 순회, 정확도 향상 등을 수행해야 한다.

이 작업을 위해 기존의 AnerimExercise 프로젝트부터 열어 참고해야 한다. 이 프로젝트가 거의 원본이어서 참고할게 많다. 최소한 샘플 텍스트라도 빼 와야 한다. 통계는 속도를 위해 멀티 스레드로 돌려야 하고 용도가 분명하므로 별도의 프로젝트로 만드는 것이 좋다. 거의 10년만에 비주얼 스튜디오 2019로 프로젝트를 열었더니 변환하겠다는 안내 메시지가 나타난다.

다행히 변환 후 바로 컴파일 가능했다. Win32 프로젝트로 되어 있으며 유니코드 설정은 되어 있지 않다. 샘플은 SampleEng.txt를 사용한 거 같다. 코드는 차근히 다시 분석해 봐야겠는데 아무리 10년이 지났어도 내가 만든 프로젝트이니 딱히 어려울 거 같지는 않다.

통계 프로젝트의 이름은 최초 EnglishKeyboardLayout으로 했다가 차후 영어가 아닌 다른 언어용으로도 쓸 수 있을 거 같아 English는 빼고 "평가, 나열" 같은 단어를 쓰기로 했다. Enum, Estimate, Evaluate 등 여러 후보를 떠올리다 Ranking으로 낙점했다. 수억개의 키보드 레이아웃에 대해 랭킹을 매기니 적당한 이름인 거 같다.

RankingKeyboardLayout 프로젝트를 Win32 템플릿으로 생성하고 단독 실행 파일, 유니코드 사용 안함, SDL 옵션 끔 등의 옵션을 설정했다. ApiStart.txt 파일 가져와 일단 컴파일하고 메시지 크래커로 프로젝트 형태를 만들어 띄웠다. 껍데기 다 만들었고 점수를 매기는 방법을 하나씩 구현한다.

2147- Ranking 초안

Ranking의 초안 구현예는 다음과 같다. 샘플은 루프돌며 계속 사용해야 하므로 7K, 5800자 정도로 짧게 줄여 리소스에 포함시켰다. 4개의 배치에 대해 점수를 점검하는 형식이며 기존 통계 프로그램의 코드를 가져와 점수를 내도록 조정했다.

초안의 각 레이아웃 점수는 다음과 같다. 빈도순의 부담도가 0이고 1차배치의 손가락 연타와 10대 연철이 0점인걸로 봐서 제대로 평가는 하고 있는거 같은데 비율이 영 맞지 않다.

 

레이아웃

부담도

좌우

손가락

손가락연타

손연타

10대연철

총감점

쿼티

194

8

43

23

10

25

20

323

빈도순

0

6

26

25

50

55

46

208

알파벳

138

1

28

40

50

60

60

377

1차배치

15

2

21

10

0

40

0

88

 

이제 이걸로 레이아웃 루프를 돌며 점수를 매기고 상위 1000개 정도의 후보 배치를 찾아내면 된다. 점수를 내는 방식이 너무 초안이라 변별력이 떨어진다. 우선 부담도부터 조정하여 손가락 사이의 차별성이 떨어지는 거 같아 다음과 같이 조정했다.

1행보다 3행이 조금 더 높은 것으로 하고 집안열의 부담도를 더 높였다. 새끼 손가락의 위아래보다 집게 안쪽을 누르는게 자세가 틀어져 더 어렵다고 평가했다. 이렇게 조정하니 빈도순의 부담도 평균이 14.49가 되고 1차 배치는 14.59가 된다. 이 정도 수준을 만점으로 잡아야 할 거 같다. 만점보다 약간 더 높은 14.4를 기준으로 했다.

쿼티는 19.25, 알파벳순은 19.04로 최대 5.3 정도까지 멀어진다. 100점 만점으로 하여 부담도의 10배 정도를 감점한다. 부담도가 높으면 50점 정도까지 점수가 깍이도록 한다. 부담도에 따라 나머지 평가 기준도 상관 관계가 있어 가장 크게 영향을 미친다.

엄지 제외 좌우 분담율은 53:47을 만점으로 하되 +-2까지는 감점하지 않고 그 이상은 제곱으로 감점한다. 왼손에 부담을 좀 더 준 이유는 오른손은 편집, 마우스까지 감당하기 때문이다. 50:50이나 56:44이면 1점 감점이지만 48:5258:42이면 9점이나 깍인다. 이 기준으로는 쿼티만 -3점이고 나머지는 모두 0점이다.

손가락 분담률은 15:14:14:6을 만점으로 하고 +-1까지는 무감점, 그 이상은 가중치없이 초과한만큼 감점한다. 쿼티는 6:7:16:19-16:7:12:2로 좌우 손가락 분담률이 비대칭이라 감점이 많고 빈도순으로 배치하면 6:9:12:22-21:11:10:1로 너무 안쪽으로 몰려 역시 분담률이 좋지 않고 1차 배치도 오른손 새끼의 부담률이 낮아 5점 감점이다.

행분담률은 27:55:18을 만점으로 하되 +-2까지는 무감점, 그 이상은 초과한만큼 감점으로 계산했다. 만점 자체가 주관적이고 임의적이다 보니 정확한 평가가 참 어려운데 가급적 중앙행에서 많이 입력하는 것이 좋다는 뜻일 뿐 사실 이대로 분담되기는 어렵다. 부담도와 상관관계가 있어 허용치를 5로 넓게 주었다. 쿼티는 51:31:171행의 비율이 비정상적으로 높다. 빈도순은 24:58:172점 감점이며 1차 배치는 33:52:148점 감점이다.

손가락 연타율과 손연타율은 부담도와 상관 관계가 없어 잘 측정해야 한다. 각각 5%, 40%를 만점으로 하고 초과분에 대해 3을 곱해 감점했다. 만약 지정한 비율보다 더 낮으면 마이너스 감점도 있어 낮을수록 좋다. 예상외로 쿼티의 연타율이 낮아 3점 감점이고 1차 배치가 0점으로 감점이 없다. 빈도순과 알파벳순은 의도없이 순차배치한 것이어서 감점이 높다.

마지막으로 10대 연철에 대해서는 er, th, eh13점으로 in, es5, it, or3, no, en, de1점으로 심각도에 따라 감점을 많이 주었다. 앞쪽 셋에 대한 연철이 있으면 대폭 감소시켜 거의 과락에 해당하는 불이익을 준다. 이 부분에 대해서도 쿼티의 연철이 낮으며 1차 배치는 무감점이다. 여기까지 조정한 기준으로 감점 정도를 조사해 보면 다음과 같다.

 

레이아웃

부담도

좌우

손가락

손가락연타

손연타

10대연철

총감점

쿼티

48

3

20

45

3

0

1

120

빈도순

0

0

26

2

15

18

16

77

알파벳

46

48

45

31

18

12

3

203

1차배치

1

0

5

8

0

9

0

23

 

점수에서 제일 중요한 것은 변별력이다. 이 결과로 보면 1차 배치가 역시 점수가 제일 높고 빈도순은 부담도만 낮을 뿐 분담률이나 연철에서 안 좋은 결과가 나타난다. 변별력은 있는 셈이나 1차배치보다 더 좋은 배치를 찾아내는 것이 쉽지 않을 거 같다.

100점 만점으로 맞추려고 했으나 굳이 그럴 필요는 없을 거 같다. 1차 배치를 기준으로 하여 조금씩 변화를 줘 가며 점수를 매겨보면 20~50점 사이를 왔다갔다 할 거 같고 100점 이상 감점인 배치는 과락으로 보면 된다.

기준을 더 조정해 봐야 별반 달라질 거 같지 않아 일단 여기까지 기준을 마련하고 배치를 순회하며 무수히 많은 배치에 대해 점수를 매겨 보고 감점이 낮은 탑 100을 찾아 봐야겠다. 오늘은 419일이다. 47일 시작한 점수 작업이 12일이나 걸린 셈인데 그간 출판 일정도 있었고 봄맞이 꽃놀이도 좀 다녀오느라 작업을 많이 하지 못했기 때문이다. 이제 슬슬 속도를 좀 내 봐야겠는데 요즘 따라 체력이 더 떨어져 얼마나 할 수 있을지 모르겠다. 서두르기는 하되 조급해하지는 말자.

21421- miryoku 분석, 순회 연구

키보드에 대한 더 많은 정보를 얻기 위해 클리앙 키보드당에 입당했다. 키보드매니아도 물론 활동중인데 요즘은 많이 활성화되어 있지 않은 모양이다. 클리앙의 글을 읽다가 30% 배열이라는 miryoku라는 배열에 대한 소개글을 보고 깜짝 놀랐다.

어쩜 이렇게 내가 만든 키보드랑 거의 똑같은지, 역시 사람들이 생각하는건 다 비슷한건가 싶기도 하고 빨리 완성해야겠다 싶은 생각도 들었다. 특허를 내기 전에 일단 나도 이 시점에 이런 배열을 생각하고 있었음을 기록으로 남기기 위해 댓글로 내가 만든 배열도 올려 두었다. 관련 홈페이지는 다음과 같다.

 

https://github.com/manna-harbour/miryoku

 

엄지 3, 각 손가락 3행이되 단 집게만 안쪽열 있는 구조로 제자리에서 모든 입력을 처리하겠다는 생각은 나랑 같다. 그러나 조합키가 대칭인 점, 언어 전환키가 없는 점, 편집키가 일자인 점, 숫자키가 왼손인 점 등은 나와 다르다. Num, Sym 등은 아마도 홀드 다운 상태로 누르는 모양인데 이걸 Tap-Mod라고 칭한다.

Shift, Ctrl, Alt도 마찬가지 방식이며 좌우에 대칭적으로 가장 좋은 위치에 배치해 두었다. 괜찮은 생각인거 같지만 WHAM과 같이 좌우 교대로 있는 알파벳을 대문자로 입력하려면 양손 다 춤을 춰야 하는 불편함이 있을 듯 하다. Shift는 엄지에 두는 것이 더 맞는 거 같다. 레이어가 6개나 있는 것도 익숙해지기 어려울 거 같다. 실물 키보드도 만들어서 파는 것 같다.

 

인체 공학적이라고는 하나 굳이 이렇게 좌우 분리형으로 따로 만들 필요가 있을까 싶다. 오히려 밀리고 책상위가 지저분해지는 불편함이 있을 거 같다. 같은 뭉치이되 거리나 각도를 조절할 수 있는 형태가 더 나아 보인다.

나와 똑같은 생각을 한 사람이 있다는 것은 반가운 일이면서도, 나보다 먼저 만들어 버리면 어쩌나 싶은 생각도 든다. 막상 구현예를 보니 기본 형태는 비슷해도 세부적으로 다른 부분이 더 많은 거 같아 나만의 키보드를 계속 만들 수 있을 거 같다. 나도 충분히 연구했고 더 잘만들 수 있을 거 같은데 공상만 하지 말고 빨리 만들어야겠다는 생각이 든다.

------------------------

점수표는 다 만들었고 이제는 여러 가지 배열에 대해 점수를 매겨보고 괜찮은 배열을 찾아야 한다. 제일 먼저 부닥친 문제는 배열을 순회하는 것이다. 전에 연구해 봤다시피 뒤쪽 4개를 빼고도 11해의 배열을 순회하는 것은 무모한 짓이다. 가능성이 있는 배열에 대해서만 순회할 필요가 있다.

그러기 위해서는 일단 순회 알고리즘을 연구해야 하는데 이것만 해도 그리 간단하지가 않다. 순열 알고리즘이 보기보다 복잡해 C#이나 자바에는 해당 기능이 없고 C++에는 있다. 먼저 순회 알고리즘부터 정확히 이해해야 한다. 가장 간단한 알고리즘은 재귀를 통해 순열을 구하는 것이다.

 

#include <iostream>

#include <algorithm>

#include <time.h>

using namespace std;

 

// 배열 범위를 벗어날 수 있다는 경고 제거

#pragma warning(disable:6385)

#pragma warning(disable:6386)

#pragma warning(disable:4996)

 

#define Count(a) (sizeof(a)/sizeof(a[0]))

#define SWAP(x,y,t) {t=x;x=y;y=t;}

#define ABS(a) ((a) >= 0 ? (a):-(a))

 

char ar[] = "abc";

int last;

char t;

 

void EnumLayout(int st, int ed)

{

    for (int i = st; i <= ed; i++) {

        SWAP(ar[st], ar[i], t);

        if (st + 1 == ed) {

            puts(ar);

        } else {

            EnumLayout(st + 1, ed);

        }

        SWAP(ar[st], ar[i], t);

    }

}

 

int main()

{

    last = strlen(ar) - 1;

    // 재귀하는 방법

    EnumLayout(0, last);

}

 

속도와 논리의 간단함을 위해 대상과 주요 변수는 전역으로 선언했다. "abc" 세 문자에 대한 순열은 3! = 6가지이다. 이 목록을 구하는 알고리즘이 EnumLayout이다. 대상은 전역 변수 ar이며 이 배열의 원소를 교환해가며 순열을 구한다. 최초 EnumLayout(0, 2)로 순열의 처음과 끝 위치를 지정한다. 이후 다음 과정으로 순열을 구한다.

 

EnumLayout(0, 2)

  for i = 0 ~ 2

    i = 0위치(a) 0위치(a) - abc

    EnumLayout(1, 2)

      for i = 1 ~2

        i = 1 : 1위치(b) 1위치(b) - abc

        abc 출력

        1위치(b) 1위치(b) - abc

        i = 2 : 1위치(b) 2위치(c) - acb

        acb 출력

        1위치(c) 2위치(b) - abc

     0위치(a) 0위치(a) - abc

     i = 1 : 0위치(a) 1위치(b) - bac

     EnumLayout(1, 2) - 위 과정 반복하며 bac, bca 출력

     0위치(b) 1위치(a) - abc

     i = 2 : 0위치(a) 2위치(c) - cba

     EnumLayout(1, 2) - 위 과정 반복하며 cba, cab 출력

 

EnumLayout(1, 2)만 보면 bc에 대해 b b, b c를 통해 정순서, 역순서 둘을 출력한다. 루프 처음의 sti가 같을 때는 동일 위치를 교환하는 쓸데없는 짓을 하는 셈인데 최초 상태를 출력하는 역할을 한다. 다음 루프는 첫 위치와 마지막 위치를 교환한 상태에서 출력한다. 출력 후 항상 재교환하여 원래 상태로 돌아간다.

이걸 감싸는 EnumLayout(0, 2)는 루프를 돌며 a a, a b, a c를 각각 수행하면서 EnumLayout(1, 2)를 또 호출하여 뒤의 둘을 바꿔가며 출력한다. EnumLayout(1, 2)는 항상 제일 마지막 둘에 대한 순열 2가지만 출력하고 EnumLayout(0, 2)012와 교환하면서 마지막 둘의 상태에 대한 순열을 또 출력한다. 그래서 3 * 2 = 6의 순열 개수가 나온다.

 

EnumLayoutst 위치의 요소를 뒤쪽 요소와 하나씩 교환하면서 배열 요소가 하나 더 작은 자신을 재귀 호출한다. EN(1, 2)는 제일 뒤쪽 2개에 대한 순열을 만들고 EN(0, 2)0번째 요소를 중간 중간에 끼워 넣으며 EN(1, 2)를 세 번 호출한다.

만약 "abcd"로 문자열이 네 개라면 위 그림의 왼쪽에 for i = 0 ~ 4 루프가 들어가며 a를 차례대로 a, b, c, d와 바꿔가며 위 그림의 과정을 4번 반복한다. 그래서 24개의 순열이 나온다. "abcde"라면 왼쪽에 for i = 0 ~ 5 루프가 추가되어 5배로 늘어난다. 이런 식으로 순열을 만들어 낸다.

이 방법은 이해하기는 쉽지만 SWAP이 너무 많아 느리다. 그래서 1963년에 Heap이라는 사람이 교환횟수를 줄인 더 효율이 좋은 HeapPermutation 알고리즘을 만들었다. 목록의 크기가 홀인가, 짝인가에 따라 교환 대상을 선택하는 식인데 자세히 분석해 보지는 않았다.

키보드와 직접적인 상관은 없지만 전체 순열을 다 구하는 방법 말고 일부에 대한 순열을 구하는 방법도 정리해 두자. n개 중에서 r개를 뽑는 즉, nPr을 구하는 함수는 다음과 같다. 다섯 개의 알파벳 중에 3, 5P3을 뽑는다.

 

char ar[] = "abcde";

int last;

char t;

 

void EnumLayout(int st, int ed)

{

    for (int i = st; i <= ed; i++) {

        SWAP(ar[st], ar[i], t);

        if (st + 1 == ed) {

            puts(ar);

        } else {

            EnumLayout(st + 1, ed);

        }

        SWAP(ar[st], ar[i], t);

    }

}

 

void Permutation(int n, int r, int depth)

{

    if (r == depth) {

        for (int i = 0; i < depth; i++) {

            printf("%c", ar[i]);

        }

        printf("\n");

    }

 

    for (int i = depth; i < n; i++) {

        SWAP(ar[i], ar[depth], t);

        Permutation(n, r, depth + 1);

        SWAP(ar[i], ar[depth], t);

    }

}

 

int main()

{

    Permutation(strlen(ar), 3, 0);

}

 

길이와 뽑을 개수, 그리고 깊이를 전달하되 최초 호출시는 Permutation(strlen(ar), 3, 0) 식으로 depth0으로 준다. 내부에서 재귀 호출을 통해 depth를 점점 늘려 가며 r개가 될 때 하나씩 출력하는 형식이다. 대충 실행되는 것만 확인했으며 자세한 분석까지는 해 보지 않았다. 차후 필요하면 분석해 보기로 한다.

재귀에 의한 순열은 직관적이지만 중간에 멈출 수 없고 모든 순열을 다 뽑을 수만 있다. 재귀의 스택 상태를 인위적으로 만들기 어려워 특정 순열에서부터 시작할 수가 없다. 이에 비해 임의의 순열에 대해 다음 순열을 찾는 알고리즘도 있다. 이 방법은 재귀를 쓰지 않고 현재 배열에 기반하여 다음 순열을 찾아내는데 C++STL 라이브러리에도 포함되어 있다. 이 방법을 쓰면 간단하게 순열을 순회할 수 있다.

 

int main()

{

    char ar[] = "abcde";

    do {

        puts(ar);

    } while (next_permutation(&ar[0], &ar[strlen(ar)]));

}

 

인수로 시작지점의 포인트와 끝 다음 지점의 포인트를 전달하면 이 두 지점 사이의 요소를 읽어 다음 순열을 찾는다. 잘 동작하지만 소스를 보면 STL답게 참 알아 보기 어렵다. 좀 보기 쉽게 정리한 소스를 구해 다시 정리했다.

 

char ar[] = "abcde";

int last;

char t;

 

bool NextLayout()

{

    int i, j;

 

    // 왼쪽이 오른쪽보다 작은 최초의 위치 찾기

    for (i = last - 1; i >= 0; i--) {

        if (ar[i] < ar[i + 1]) break;

    }

 

    // 다 내림차순이면 순열의 끝임

    if (i == -1) return false;

 

    // i 위치값보다 더 큰 최초의 위치 찾기

    for (j = last; ar[j] <= ar[i]; j--) { ; }

 

    // i, j의 값 교환

    SWAP(ar[i], ar[j], t);

 

    // 뒷부분 오름차순으로 정렬

    for (j = last, i++; i < j; i++, j--) {

        SWAP(ar[i], ar[j], t);

    }

 

    return true;

}

 

 

int main()

{

    last = strlen(ar) - 1;

    do {

        puts(ar);

    } while (NextLayout());

}

 

 이 알고리즘은 오름차순으로 되어 있는 배열을 점진적으로 내림차순으로 전환하며 순열을 탐색한다. 임의의 순열이 주어졌을 때 다음 과정을 거쳐 바로 뒤의 순열을 구한다. 예를 들어 cfdhgeba라고 하자.

먼저 역순으로 순회하며 왼쪽이 오른쪽보다 작은 최초의 위치를 찾는다. 제일 오른쪽의 baba보다 더 크니 아니다. eb, ge, gh도 다 아니며 dh가 왼쪽이 더 작은 최초의 위치이다. 이 위치는 내림차순으로 변환되지 않은 최초의 위치이며 따라서 다음 순열을 찾을 대상이다. 이 위치의 d자리를 i라고 칭한다. i위치 이전의 문자 c, f는 더 높은 자리라 아직 건드릴 필요가 없다. 만약 i가 발견되지 않으면 전체가 내림차순이라는 뜻이므로 순열의 마지막이다.

다음은 다시 끝에서 순회하며 d보다 더 큰 최초의 문자를 찾는다. d 바로 오른쪽의 hd보다 크기 때문에 d 이전에 d보다 더 큰 문자는 반드시 존재한다. 이 경우는 h까지 갈 필요도 없이 그 전에 e를 먼저 발견한다. 이 위치를 j라고 칭한다.

현재 배열에서 한 단계 더 내림차순으로 가기 위해 i위치와 j위치의 값을 교환한다. cfehgdba가 된다. 더 작은 d를 더 큰 e와 교환하여 뒤쪽으로 보냈으므로 이전보다 더 내림차순이 되었다. 더 작은값을 뒤로 보냈으니 i 자리 이후는 여전히 내림차순이다. 그러나 교환하기 전후의 두 상태는 인접 단계가 아니라 중간에 수많은 내림차순 상태가 더 있다.

이 알고리즘은 항상 오름차순에서 내림차순으로 가므로 i위치 이후를 오름차순으로 바꾼다. 더 높은 단위를 바꾸었으므로 하위의 모든 단계를 반복하며 중간 상태를 더 구해야 한다. 천자리 숫자가 바뀌었으면 일,,백 자리는 다시 반복해야 하는 것과 비슷하다. i 위치 이후의 원소를 교환하여 뒤집는다(reverse). i+1과 마지막 요소를 두 지점이 만날 때까지 가운데로 이동하며 하나씩 교환하면 된다.

이후 오름차순으로 바꾼 하위 배열에 대해 다음 순열 구하기를 반복한다. 최초 gh만 바뀌고 그 다음은 바로 앞으로 dg와 자리를 바꾼 후 끝 두 자리를 또 바꾸어 gdh, ghd가 된다. 이런 식으로 abdgh에 대해 한단계씩 내림차순으로 이동하여 하위 배열에 대한 순열을 다 구한다.

결국 이 방법도 끝에서 둘을 교환하는 과정을 앞 문자에 대해 계속 반복하니 재귀와 비슷하다. "abc"에 대해 이 알고리즘이 적용되는 과정을 분석해 보자.

 

abc : i위치는 b, j위치는 c. 이 둘만 교환. i + 1이 혼자라 reverse는 할게 없음

acb : i위치는 a, j위치는 b가 되어 교환. bca가 되었다가 careverse

bac : i위치는 a, j위치는 c. 이 둘만 교환

bca : i위치는 b, j위치는 c. 이둘을 교환. cba가 되었다가 bareverse

cab : i위치는 a, j위치는 b. 이 둘만 교환.

cba : 내림차순이 되어 i위치가 더 없음. 종료.

 

i가 끝 두 번째일 때는 마지막 두 문자만 교환하고 reverse는 할게 없다. abc, bac, cab일 때가 그렇다. i가 끝 세번째 이후일 때는 교환 후 reverse하여 i 이후를 오름차순으로 바꿔 놓고 뒷 부분에 대해서도 중간 순열을 찾는다. acb에서 교환하면 bca가 되는데 b가 앞으로 오면서 뒷 부분이 오름차순이라 중간을 생략한 셈이다. 그래서 bac로 바꾸어 이 값을 먼저 출력하고 ac를 다시 바꾸어 bca를 출력한다.

i가 더 앞쪽일수록 하위 배열이 더 커져 순회해야할 순열의 종류가 기하급수적으로 늘어난다. 끝 두번째이면 2개밖에 없다. 세 번째이면 2개의 순열을 3번 반복하니 6, 네 번째이면 그걸 4번 반복하니 24개가 된다. 정리하자면 in번째 위치에서 발견될 때 그 하위 순열은 n!개 더 존재한다. 이런 식이니 길이가 늘어나면 순열의 수가 엄청나게 증가하는 것이다. 2의 거듭승보다 훨씬 더 많다.

순열을 구하는 방법은 터득했는데 그 수가 너무 많다. 키보드 레이아웃은 앞뒤로 몇 칸씩만 움직여 보면 되는데 그 방법을 아직 찾지 못했다. 빈도별 위치에서 앞 뒤로 두어칸씩만 이동하며 레이아웃을 평가하면 되는데 중간 과정을 생략할 수가 없다. 영 아니다 싶은 배열은 건너뛰는 방법을 찾아야 한다. 일단 적절한 레이아웃인지 아닌지부터 판별해 보자.

 

char ori[26];

int dist = 2;

 

bool TestInDist()

{

    for (int idx = 0; idx < last + 1; idx++) {

        int off = strchr(ar, ori[idx]) - ar;

        if (abs(off - idx) > dist) return false;

    }

    return true;

}

 

bool NextLayout()

{

     // 위 예제와 같음

}

 

int main()

{

    last = strlen(ar) - 1;

    strcpy(ori, ar);

    do {

        if (TestInDist()) {

            printf("%s : OK\n", ar);

        } else {

            printf("%s : PASS\n", ar);

        }

    } while (NextLayout());

}

 

이동 거리를 조사하기 위해 순회하기 전의 원래 배열을 ori에 복사해 둔다. 그리고 매 순열에 대해 ar의 모든 문자가 ori 배열의 원래 위치에서 어디에 가 있는지 조사하고 dist 이상 멀어졌는지 판별한다. dist는 일단 2로 주었다. 각 순열에 대해 적합한 레이아웃인지 판별하여 OK 또는 PASS를 출력한다. 결과는 다음과 같다.

 

abcdefg : OK

abcdegf : OK

abcdfeg : OK

abcdfge : OK

abcdgef : OK

abcdgfe : OK

abcedfg : OK

abcedgf : OK

abcefdg : OK

abcefgd : PASS

abcegdf : OK

abcegfd : PASS

abcfdeg : OK

abcfdge : OK

abcfedg : OK

abcfegd : PASS

abcfgde : OK

abcfged : PASS

abcgdef : PASS

abcgdfe : PASS

 

처음에 뒷부분만 깔짝거릴 때는 다 OK이다. abcefgd가 제일 먼저 PASS인데 d가 너무 뒤쪽으로 가 버렸기 때문이다. d가 다시 한칸 앞으로 오면 이때는 다시 OK가 되었다가 뒤로 가면 또 PASS가 된다. 뒷 부분으로 가면 전부 PASS이다.

 

dabcefg : PASS

dabcegf : PASS

dabcfeg : PASS

dabcfge : PASS

dabcgef : PASS

dabcgfe : PASS

dabecfg : PASS

dabecgf : PASS

 

첫 글자가 d이면 뒤쪽이 뭐든간에 모두 PASS가 될 수밖에 없다. d가 이미 자리를 벗어난 상태에서 하위 배열을 순회하는게 의미가 없다. 이후의 모든 순열이 다 마찬가지이다. 이걸 효율적으로 건너뛰는 방법을 찾아야 한다. 부적합 판정을 내릴게 아니라 아예 이 순열 자체를 만들 필요가 없다. 참 어려운 문제이다.

----------------------------

배열이 길어지면 순회하는데만도 시간이 엄청나게 걸린다. 그래서 불필요한 배열은 아예 건너 뛰어야 하는데 이를 위해 PASS하는 배열의 특징을 잘 관찰해야 한다. 구분하기 쉽도록 ar"12345"로 지정하고 dist2로 한 상태에서 2칸 이상 멀어지는 경우를 찾아 보았다.

 

13425 : OK                 적합

13452 : PASS            2가 제일 뒤에 있어 3칸 떨어졌음. 4위치가 i

     교환만 : 13542    i위치로 온 5가 아직은 범위 안이다. reverse가 필요하다.

     reverse : 13524     적합

13524 : OK

13542 : PASS          

14235 : OK

14253 : OK

14325 : OK

14352 : PASS

14523 : OK

14532 : PASS           2가 너무 멀어져 자연스럽게 PASS. 4위치가 i, 5위치가 j

     교환만 : 15432      i위치로 온 5가 원래 자리에서 세칸 떨어짐. reverse 불필요

     reverse : 15234

15234 : PASS

15243 : PASS

15324 : PASS

15342 : PASS

15423 : PASS

15432 : PASS           여기까지는 전부 PASS이다.

21345 : OK                여기서부터 다시 순회하면 된다.

 

앞부분은 대부분 OK이고 13452에서 처음 PASS가 발생한다. 이때 i 위치는 4이며 교환에 의해 새로 자리잡은 5도 원래 자리에서 두 칸밖에 떨어져 있지 않다. 따라서 5때문에 부적합하지는 않으므로 reverse하고 계속 순회해 봐야 한다. 이후 5 뒷부분의 교환에 의해 OKPASS가 교대로 나타난다.

14532에서 중요한 규칙을 발견할 수 있는데 i위치로 새로온 5가 원래 자리에서 이미 3칸 떨어져 부적합하다. , 51번째 자리에 있는 이상 5 뒤쪽이 어떻게 바뀌더라도 모두 PASS일 수밖에 없다. 그래서 교환만한 15432 상태에서 reverse를 생략해 버리면 오름차순으로 갔다가 다시 내림차순으로 순회하는 중간 과정을 모두 생략할 수 있다.

reverse를 생략하고 한참 아래쪽의 15432로 바로 점프해 버리는 것이다. 원래 이 위치는 432를 뒤집은 234를 한칸씩 내림차순으로 순회해야 하지만 reverse를 하지 않으면 건너뛸 수 있다. 15432에서 다음 순회를 계속하면 불필요한 배열을 건너뛸 수 있어 배열 개수가 대폭적으로 줄어든다.

여기까지 규칙을 연구한 후에 과연 잘 패스하는지, 개수는 몇 개이고 시간은 얼마나 걸리는지 테스트 코드를 작성해 보았다. 출력문 때문에 시간이 너무 오래 걸려 천만번에 한번만 결과를 보고하도록 했다.

 

#include <iostream>

#include <algorithm>

#include <time.h>

#include <conio.h>

using namespace std;

 

// 배열 범위를 벗어날 수 있다는 경고 제거

#pragma warning(disable:6385)

#pragma warning(disable:6386)

#pragma warning(disable:4996)

 

#define Count(a) (sizeof(a)/sizeof(a[0]))

#define SWAP(x,y,t) {t=x;x=y;y=t;}

#define ABS(a) ((a) >= 0 ? (a):-(a))

 

char ar[] = "abcdefghijklmnopqrstuv";

const int dist = 4;         // 허용 거리

int last;

char t;

char ori[27];

__int64 num = 1, ok = 0;

time_t t1, t2;

 

// 전체 배열이 모두 위치안에 있는지 조사한다.

inline bool TestInDist()

{

    for (int idx = 0; idx <= last; idx++) {

        // 대응 위치의 문자끼리 빼서 dist 이상 거리인지 조사.

        if (ABS(ar[idx] - ori[idx]) > dist) return false;

    }

    return true;

}

 

bool NextLayout()

{

    int i, j;

 

    // 왼쪽이 오른쪽보다 작은 최초의 위치 찾기

    for (i = last - 1; i >= 0; i--) {

        if (ar[i] < ar[i + 1]) break;

    }

 

    // 다 내림차순이면 순열의 끝임

    if (i == -1) return false;

 

    // i 위치값보다 더 큰 최초의 위치 찾기

    for (j = last; ar[j] <= ar[i]; j--) { ; }

 

    // i, j의 값 교환

    SWAP(ar[i], ar[j], t);

 

    // i위치로 교환한 문자가 범위안일 때만 reverse 실행, 아니면 건너뜀

    if (ABS(ar[i] - ori[i]) <= dist) {

        // 뒷부분 오름차순으로 정렬

        for (j = last, i++; i < j; i++, j--) {

            SWAP(ar[i], ar[j], t);

        }

    }

 

    return true;

}

 

int main()

{

    /* 중간에서부터 시작할 때. 소스에 직접 중간값을 대입한 후 시작한다. 소요 시간은 알 수 없다는 문제가 있다.

    strcpy(ar, "abcdfeglhnopmjsiktqr");

    num = 1080000000;

    ok = 80970000;

    //*/

 

    // 마지막 첨자를 찾아 놓는다.

    last = strlen(ar) - 1;

 

    // 이동 거리 측적을 위한 원본 백업

    strcpy(ori, ar);

    time(&t1);

 

    do {

        if (TestInDist()) {

            ok++;

        }

        num++;

 

        // 5000만번당 한번씩만 출력한다.

        if (num % (5000 * 10000) == 0) {

            printf("num=%d %04d OK=%d %04d layout=%s : %s\n",

                int(num / 100000000), int(num / 10000 % 10000),

                int(ok / 100000000), int(ok / 10000 % 10000),

                // bOk에 결과를 대입하는 동작 생략하고 5000만번에 한번꼴로 재검사하는게 더 빠를 듯

                // -> 실측해 보니 별 차이는 없음

                ar, TestInDist() ? "OK" : "PASS");

        }

    } while (NextLayout());

 

    time(&t2);

    int cho = (int)difftime(t2, t1);

    printf("num = %I64d, ok = %I64d, 시간=%d %d\n", num, ok, cho / 60, cho % 60);

 

    // 직접 실행해도 결과를 볼 수 있도록 잠시 입력 대기

    getch();

}

 

이 코드로 ar의 개수를 점점 늘려가며 각 dist에 대해 몇 개의 배열이나 있는지, 시간은 얼마나 걸리는지 점검해 보았다. 최초 회사에서 Ryzen 3550H CPU로 실행했는데 집에서 i7-8700으로 다시 해 보니 대략 절반 정도의 시간밖에 걸리지 않았다. r까지 dist330분 걸렸는데 데스크탑으로 하니 1522초로 딱 절반이다.

여기서 릴리즈로 컴파일해서 실행하면 또 3배 더 빨라진다. s부터는 릴리즈로 실행한 것인데 dist2는 디버그로 45, 릴리즈로 10초이다. C/C++은 디버그와 릴리즈의 실행 속도 차이가 상당하니 디버그는 개발중에만 쓰고 실제 사용할 때는 반드시 릴리즈로 실행해야 한다.

더 최신 CPU로 바꾸면 최소 2배는 더 빨라질 것 같고 실제로 지금 내 컴퓨터보다 두 배 더 빠른 CPU가 널렸다. 현재 속도로도 초당 5000만개를 순회하는 지경이나 컴퓨터가 과연 빠르기는 하다. 게다가 이 연산은 오로지 싱글 스레드만 쓰는 거라 스레드 돌리면 최소 10배는 더 빨라진다는 계산이 나온다.

 

알파벳

dist = 1

dist=2

dist=3

dist=4

a~p

 

1035, 35, 1

3.7, 1600, 1

51, 2.8, 3

a~q

 

3000, 82, 5

14, 5000, 4

250, 10, 15

a~r

 

9200, 191, 15

58, 1.5, 15

1227, 41. 73

a~s

 

2.2, 446, 10

230, 4.6. 13

6014, 157. 6시간 12

a~t

 

8.8, 1042, 30

909,14,54

2.9. 598. 24시간 30

a~u

209, 1. 0

24, 2400, 90

3592,43.3시간40

14.4. 2200. 33시간

a~v

419, 2. 0

73, 5600, 434

14200, 1332630

14시간 30

707539, 8615

15.6

a~v dist5 예상 : 3500조중 40조개 정도될 듯하며 300일 정도 예상됨.

 

dist2에서 알파벳이 하나 늘때마다 대략 3배씩 배열 개수가 늘어나고 시간도 3배 정도 더 걸린다. dist2에서 dist3으로 늘리면 배열 개수와 시간이 거의 50배 가량 늘어난다. dist4로 늘리면 대략 15배 정도 늘어난다.

필요한 길이는 뒤쪽 4개를 빼고 a~v까지인 22개 정도면 충분하다. 앞쪽의 E, T는 자리 고정해 놓고 좀 더 줄일 수도 있지만 이 정도 속도면 다 포함시켜도 충분할 것 같다. dist의 최적값은 좀 더 고민해 봐야겠는데 2이면 좌우 2칸씩까지 해서 5칸을 움직여 보는 것이고 3이면 7칸을 움직여 보는 것이다. dist 4까지는 굳이 가볼 필요가 없을 것 같은데 차후 천천히 시도해 보기로 하자.

dist3일 때는 22개 배열은 아직 실측해 보지 않았지만 비율상으로 계산해 보면 대략 25억개의 배열에 대해 30시간 정도 순회해야 할 것 같다. 이 시간은 단순히 순회만 하는 시간이며 점수를 내는 시간은 아직 계산하지 않았다. 레이아웃 하나의 점수를 내는데 1초가 걸린다면 대략 8년이 걸리는 셈이다. 스레드 8개를 돌려도 1년이 걸리는데 시간을 좀 더 단축시켜야 한다. -> 실제 순회해 보니 133억개로 늘어났지만 시간은 14시간 정도밖에 걸리지 않았다.

그래도 너무 오래 걸린다. dist2로 낮추고 최적화를 좀 더 한 후 스레드를 왕창 돌려야 한달 정도 걸린다는 계산이다. 앞부분은 변화가 덜하므로 대략 며칠만 돌려 봐도 베스트 100안에 꽤 괜찮은 배열을 찾을지도 모른다. 당장은 dist 2 이상은 시도해 보기 어려울 것 같다. 이래서 과학 연구에 슈퍼 컴퓨터가 필요한 모양이다.

-------------------------------

레이아웃을 단순히 순회하고 개수를 조사하는데만도 년 단위의 시간이 걸리는 것에 일단 좀 좌절했다. 아무리 테스트 프로그램이고 최적화에 신경쓰지 않았다고 해도 시간이 과도하게 오래 걸린다. 그래서 좀 더 빠르게 조사하는 방법을 찾아 보기로 했다.

제일 먼저 한 생각은 char 타입보다는 int 타입이 더 빠를거라는 것이다. 32비트 머신은 32비트를 제일 잘 다루고 8비트씩 잘라서 쓰면 오히려 더 느려지는 것이 상식이다. 그래서 최종 프로그램은 int 타입으로 순회해야겠다고 생각했다.

그러자면 위의 알고리즘을 타입별로 두 벌 만들어야 한다. 순열 순회 알고리즘은 두 값을 교환하는 swap을 많이 사용하는데 이 함수가 타입 독립적이면서 빨라야 한다. 그래서 표준 swap 메서드를 써 보거나 좀 더 빠른 방법을 찾아 보기로 했다. 다음이 테스트 예제이며 다섯 개의 교환 알고리즘을 비교한다.

 

#include <iostream>

#include <algorithm>

#include <time.h>

using namespace std;

 

#define SWAP(x,y,t) {t=x;x=y;y=t;}

 

void inline Swap(int& left, int& right)

{

     int t;

     t = left;

     left = right;

     right = t;

}

 

int main()

{

     time_t t1, t2;

     int left = 3, right = 4;

 

     // STL swap 함수

     time(&t1);

     for (int i = 0; i < 1000000001; i++) {

          std::swap(left, right);

     }

     time(&t2);

     printf("swap : left=%d, right = %d, 시간=%d\n", left, right, (int)difftime(t2, t1));

 

     // 직접 만든 Swap 함수

     time(&t1);

     for (int i = 0; i < 1000000001; i++) {

          Swap(left, right);

     }

     time(&t2);

     printf("Swap : left=%d, right = %d, 시간=%d\n", left, right, (int)difftime(t2, t1));

 

     // 매크로 함수

     time(&t1);

     int t;

     for (int i = 0; i < 1000000001; i++) {

          SWAP(left, right, t);

     }

     time(&t2);

     printf("SWAP : left=%d, right = %d, 시간=%d\n", left, right, (int)difftime(t2, t1));

 

     // 직접 교환

     time(&t1);

     for (int i = 0; i < 1000000001; i++) {

          t = left; left = right;    right = t;

     }

     time(&t2);

     printf("직접 : left=%d, right = %d, 시간=%d\n", left, right, (int)difftime(t2, t1));

 

     // +/-

     time(&t1);

     for (int i = 0; i < 1000000001; i++) {

          left = left + right; right = left - right; left = left - right;

     }

     time(&t2);

     printf("+/- : left=%d, right = %d, 시간=%d\n", left, right, (int)difftime(t2, t1));

 

     // XOR

     time(&t1);

     for (int i = 0; i < 1000000001; i++) {

          left = left ^ right; right = left ^ right; left = left ^ right;

     }

     time(&t2);

     printf("XOR : left=%d, right = %d, 시간=%d\n", left, right, (int)difftime(t2, t1));

}

 

각 알고리즘으로 두 개의 정수를 교환하되 11번 반복하여 실제 값이 바뀌도록 했다. 이 예제를 만들 때 STL의 고도로 최적화된 알고리즘이 좀 더 빠르지 않을까 내심 기대했다. 그러나 실행 결과를 보면 완전 뒷통수를 맞는 느낌이다.

 

swap : left=4, right = 3, 시간=54

Swap : left=3, right = 4, 시간=14

SWAP : left=4, right = 3, 시간=2

직접 : left=3, right = 4, 시간=2

+/- : left=4, right = 3, 시간=4

XOR : left=3, right = 4, 시간=3

 

STLswap 함수가 제일 느리다. 소스를 찾아 보면 템플릿으로 타입에 독립적이며 inline 지정까지 되어 있고 레퍼런스 인수를 받는다. 그러나 내부에 이상한 매크로로 떡칠이 되어 있는데 아마도 객체인 경우 필요한 조치를 취하기 위한 코드인 것 같다. 타입 독립적으로 만들다 보니 속도는 그야 말로 형편없다. 원래부터 STL을 좋아하지 않았지만 이렇게 엉망일 줄은 예상하지 못했다.

그렇다면 STL의 불필요한 코드를 걷어 내고 직접 int, char 타입에 대한 Swap만 두 벌 만들면 어떨까 싶어 만들어 보았다. STL에 비해서는 세 배 정도 더 빠른데 불필요한 코드가 제외되어서 그렇다. 이 결과를 보고 타입 독립이고 뭐고 STL은 개나 줘 버려라는 생각이 더 굳어지게 되었다.

기존에 사용하던 SWAP 매크로는 단 2초밖에 걸리지 않는다. 역시 단순 무식한게 최고다. 다만 매크로는 임시 변수 t를 외부에 별도로 선언해 놓아야 하고 인수로도 전달해야 한다는 점에서 깔끔하지 못하지만 매번 지역 변수를 새로 만드는 것보다는 확실히 빠르다. 매크로 대신 직접 코드를 전개하는 것도 결과는 당연히 같다. 이게 다르면 말이 안된다.

다음으로 두 값을 +, - 연산자로 더하고 빼는 방법과 XOR 연산의 특수함을 이용하는 방법이 있는데 임시 변수가 없어도 된다는 장점은 있지만 그 뿐이다. 코드가 짧은 것도 아니고 속도가 더 빠른 것도 아니다. 좀 신기해 보일 뿐이다. 결론적으로 두 값을 교환할 때는 역시 temp를 거쳐 상호 대입하는 방법이 가장 빠르다.

21426- 정수 배열로 순회

다음 최적화는 char 타입 대신 int 타입으로 바꾸어 순회하는 것이다. 간단한 예제를 만들어 테스트해 보니 정수가 20% 더 빠르다. 그러나 이는 순회 속도일 뿐 체점 속도는 아니어서 별 의미가 없을지도 모른다. 어차피 순회 시간의 몇 만배를 들여 채점을 해야 하므로 순회가 빠르다고 해서 전체 체점이 빨라지는 것은 아니다.

그러나 내부 순회를 알파벳으로 하고 키보드 배열도 알파벳이면 헷갈릴 거 같다. 레이아웃은 etaon... 순의 빈도별로 평가하는데 비해 순회는 오름차순으로 해야 하기 때문이다. 그래서 내부 순회는 빈도별 키와 일대일로 대응시킨 정수로 하기로 결정했다. 정수 순열로부터 키보드 배열을 만들어 내면 된다.

순회 논리가 완성되었으니 이제 평가 준비를 한다. CPU를 충분히 활용하기 위해 스레드를 돌리고 각 스레드가 하나씩의 레이아웃을 평가하며 채점 결과를 수집한다. 순회는 숫자로 하고 스레드는 숫자 배열을 알파벳 레이아웃으로 바꿔서 사용한다. 전체 소스는 다음과 같다.

 

#include <iostream>

#include <algorithm>

#include <time.h>

#include <windows.h>

using namespace std;

 

// 배열 범위를 벗어날 수 있다는 경고 제거

#pragma warning(disable:6385)

#pragma warning(disable:6386)

#pragma warning(disable:4996)

// 열거형 대신 열거형 클래스를 사용하라는 경고

#pragma warning(disable:26812)

 

#define Count(a) (sizeof(a)/sizeof(a[0]))

#define SWAP(x,y,t) {t=x;x=y;y=t;}

#define ABS(a) ((a) >= 0 ? (a):-(a))

#define random(n) (rand()%n)

 

int ar[26];

int ori[26];

char bindo[] = "etaonshrdlicumfgwypbvkjxqz";

int t;

__int64 id = 0;

// 순회 범위 지정. 21이면 마지막 4개의 숫자는 순회하지 않으며 뒤쪽 알파벳 jxqz는 고정이다.

const int LAST = 21;

const int DIST = 2;         // 허용 거리

// 스레드의 개수. 실행 장비의 성능에 따라 적당히 결정한다.

const int THREADS = 8;

const int NUMBEST = 10;

 

// 스레드의 상태

enum eStatus

{

    IDLE,       // 놀고 있음

    RUN,        // 채점중

    END         // 채점 끝

};

 

// 스레드 하나의 평가 작업 기록

struct sEvaluate

{

    eStatus status;     // 현재 상태

    __int64 id;

    char layout[27];

    DWORD idThread;

    HANDLE hThread;

    int score;

};

 

// 스레드 배열

sEvaluate arEvaluate[THREADS];

 

struct sBest

{

    __int64 id;

    char layout[27];

    int score;

};

 

// 100개의 BEST 레이아웃

sBest arBest[NUMBEST];

 

// 전체 배열이 모두 위치안에 있는지 조사한다.

bool TestInDist()

{

    int off;

    for (int idx = 0; idx < LAST + 1; idx++) {

        // 원래 문자가 현재 배열의 어디쯤에 있는지 조사

        for (off = 0; off < Count(ar); off++) {

            if (ar[off] == ori[idx]) break;

        }

        if (ABS(off - idx) > DIST) return false;

    }

    return true;

}

 

// i위치의 문자가 범위 안인지 조사한다.

bool TestInDist(int i)

{

    // i 위치의 현재 문자가 원래 있었던 위치 구함

    int off;

    for (off = 0; off < Count(ar); off++) {

        if (ori[off] == ar[i]) break;

    }

    // 원래 위치에서 dist 초과로 멀어졌으면 범위 밖이다.

    if (ABS(off - i) > DIST) {

        return false;

    }

 

    return true;

}

 

bool NextLayout()

{

    int i, j;

 

    // 왼쪽이 오른쪽보다 작은 최초의 위치 찾기

    for (i = LAST - 1; i >= 0; i--) {

        if (ar[i] < ar[i + 1]) break;

    }

 

    // 다 내림차순이면 순열의 끝임

    if (i == -1) return false;

 

    // i 위치값보다 더 큰 최초의 위치 찾기

    for (j = LAST; ar[j] <= ar[i]; j--) { ; }

 

    // i, j의 값 교환

    SWAP(ar[i], ar[j], t);

 

    // i위치로 교환한 문자가 범위안일 때만 reverse 실행, 아니면 건너뜀

    if (TestInDist(i) == true) {

        // 뒷부분 오름차순으로 정렬

        for (j = LAST, i++; i < j; i++, j--) {

            SWAP(ar[i], ar[j], t);

        }

    }

 

    return true;

}

 

// 배열을 평가한다.

DWORD WINAPI EvaluateThread(LPVOID prc)

{

    sEvaluate* pEvaluate = (sEvaluate*)prc;

 

    for (int i = 0; i < 100000000; i++) { ; }

    srand(time(NULL));

    pEvaluate->score = random(100);

    pEvaluate->status = END;

    return 0;

}

 

// 여기서 키보드 레이아웃을 채점한다.

void EvaluateLayout(int tidx)

{

    // 순열을 레이아웃으로 변경한다.

    for (int i = 0; i < Count(ar); i++) {

        arEvaluate[tidx].layout[i] = bindo[ar[i]];

    }

    // 일련 번호 부여

    arEvaluate[tidx].id = id;

 

    // 실행중으로 변경

    arEvaluate[tidx].status = RUN;

 

    // 스레드 생성

    arEvaluate[tidx].hThread = CreateThread(NULL, 0, EvaluateThread, &arEvaluate[tidx], 0, &arEvaluate[tidx].idThread);

}

 

int main()

{

    int tidx;

    int minscore, minidx;

 

    // 작업 배열 초기화

    for (int i = 0; i < THREADS; i++) {

        arEvaluate[i].status = IDLE;

    }

 

    // 순열과 이동 거리 측정을 위한 원본 순열을 초기화한다.

    for (int i = 0; i < Count(ar); i++) {

        ar[i] = ori[i] = i;

    }

 

    do {

        if (TestInDist()) {

            id++;

 

            // 노는 스레드를 찾는다.

            for (;;) {

                tidx = -1;

                for (int i = 0; i < THREADS; i++) {

                    if (arEvaluate[i].status == IDLE)

                    {

                        tidx = i;

                        break;

                    }

                }

 

                // 찾았으면 루프 탈출하여 평가 시작

                if (tidx != -1) break;

 

                // 못찾았으면 잠시 쉰 후 작업을 마친 스레드가 있는지 조사한다.

                Sleep(20);

 

                // 작업을 마친 스레드의 결과를 취합한다.

                for (int i = 0; i < THREADS; i++) {

                    if (arEvaluate[i].status == END)

                    {

                        printf("%I64d(%s) %d\n", arEvaluate[i].id, arEvaluate[i].layout, arEvaluate[i].score);

                        // 베스트 배열에서 최소점수를 가진 배열과 첨자를 찾는다.

                        minscore = 1000;

                        minidx = -1;

                        for (int i = 0; i < NUMBEST; i++) {

                            if (arBest[i].score < minscore) {

                                minscore = arBest[i].score;

                                minidx = i;

                            }

                        }

 

                        // 최소 점수보다 더 높으면 베스트의 최소 점수 자리와 교체한다.

                        // 동점이면 굳이 교체할 필요가 없다.

                        if (arBest[minidx].score < arEvaluate[i].score) {

                            arBest[minidx].id = arEvaluate[i].id;

                            strcpy(arBest[minidx].layout, arEvaluate[i].layout);

                            arBest[minidx].score = arEvaluate[i].score;

                        }

 

                        // 다시 빈 상태로 만든다.

                        CloseHandle(arEvaluate[i].hThread);

                        arEvaluate[i].status = IDLE;

                    }

                }

            }

 

            // 채점한다.

            EvaluateLayout(tidx);

        } else {

            // 부적합 배열은 조용히 건너뛴다.

        }

    } while (NextLayout());

}

 

main에서 루프를 돌며 적합 배열에 대해 노는 스레드를 찾아 할당한다. 노는 스레드가 없으면 다른 스레드가 빌 때까지 한 퀀텀 쉬면서 대기한다. 채점이 끝난 스레드도 주기적으로 찾아 결과를 취합한다. 레이아웃의 총 개수는 그다지 중요하지 않으며 적합 레이아웃에 대해서만 id1씩 증가시키며 할당한다.

sEvaluate 구조체 하나가 스레드 하나에 할당되며 이 구조체에 체점 결과를 저장한다. 스레드 개수만큼의 구조체 배열을 만들고 각 스레드가 하나씩 맡아서 채점한다. 점수는 일단 난수로 넣었다. 평가 구조체의 상태는 비거나, 실행중이거나 끝났거나 셋 중 하나이다.

채점 결과는 arBest 배열에 고득점을 한 일정 개수의 배열을 수집하는 것이다. 속도를 위해 정렬은 하지 않으며 최저점을 덮어쓰는 식으로 구현했다. 어차피 보여줄 때 정렬해서 보여 주면 되므로 미리 정렬할 필요까지는 없다.

여기까지 순회에 대한 기본 로직을 완성했다. 콘솔 화면이라 결과를 깔끔하게 보여줄 수는 없지만 기본 순회루틴은 다 만든 것 같다. 이제 메인으로 가져가 그래픽 환경에서 스레드를 돌리며 실제 체점을 해 봐야 하는데 문제는 역시 속도이다. 현재 작성된 코드로 실측해 보면 초당 149개의 레이아웃을 채점할 수 있으며 스레드 8개를 돌린다고 하면 초당 1200개이다. 이 계산대로라면 예상 시간은 다음과 같다.

 

dist=2일 때 5600만개 0.54

dist=3일 때 133억개 128

 

처음 예상했던 것보다는 조금 더 빠르다. dist3이면 4개월이나 걸리는 셈인데 좀 더 최적화해서 1개월 안에 끝내도록 해 봐야겠다. 이 프로젝트는 이걸로 종료하고 코드 가져가 Ranking 프로젝트에 합친다. 순회 방법은 확실히 연구해 본 셈이다.

21430- CPU 성능 조사

4월 내도록 순열 순회 문제를 풀었다. 사실 회사일도 바쁘고 개인적으로도 바빠 평일에는 거의 손을 데지 못하고 주말에만 짬짬이 문제를 풀었는데 집중해서 일을 하는게 아니다 보니 꽤 오랜 시간이 걸렸다. 이걸 완성해서 영문 배열이라도 확정해야 특허라도 제출할 수 있고 그래야 시제품을 만들어 볼 수 있어서이다.

어쨌건간에 조금 허술하지만 순회 루틴은 다 완성한 거 같고 처음 예상했던 것보다는 속도가 잘 나오는 셈이다. 다 만들어 놓은 루틴을 Ranking 프로젝트에 가져다 붙였다. 약간의 버그가 더 있었지만 수정해 가며 붙였다. 그래픽 환경에 맞게 출력 루틴은 사용자 정의 메시지로 처리하고 스레드를 최대한 활용할 수 있도록 했다.

잠시 정지, 주기적으로 결과 출력, 스레드 안전 종료, 중간에서 재시작할 수 있도록, 만단위마다 콤마 넣기, 화면 레이아웃 정비 등 편의 장치도 여러 개 장만해서 붙였다. 스레드를 다루는 것이 쉽지 않지만 Win32 경험이 풍부하고 저번에 만들었던 동영상 재생기에서 스레드를 많이 써 봐서 큰 문제는 없었다. 역시 윈도우 개발은 어디 가서 좀 한다는 소리 해도 될 거 같다.

최초 SpeedTest 기능을 넣고 테스트했을 때는 스레드를 쓰고도 초당 1200개 정도의 레이아웃을 채점했는데 1초에 하나 정도로 예상했던 것에 비해서는 엄청난 속도이다. 여기다 최적화를 좀 더 하고 출력을 자재했더니 더 빨라졌다. 게다가 스레드를 동원하고 릴리즈 모드로 컴파일하니 극적으로 속도가 개선되었다.

 

디버그 : 초당 3100

릴리즈 : 초당 28000

 

이 정도 속도면 억단위라 해도 몇 시간 안에 끝낼 수 있는 양이다. 더 이상의 최적화를 할 필요는 없을 거 같고 논리적으로 틀린 부분만 찾아 정확한 결과를 도출해 내면 될 것 같다. dist3으로 전체 레이아웃을 다 돌려본 결과는 다음과 같다.

 

200,0124      (etanoshdrcliufgmywvkpbjxqz) 79.85점 좌우 55:45 손가락 6:11:14:18-23:11:11:3 28:55:15 손가락연 6% 손연 40% 연철 1

343,5540      (etasonhdrcliufgmywvkpbjxqz) 79.83점 좌우 55:45 손가락 6:11:14:18-23:11:11:3 28:55:15 손가락연 6% 손연 40% 연철 1

235,1645      (etanohsdrcliufgmywvkpbjxqz) 79.60점 좌우 56:44 손가락 6:10:13:18-23:11:10:3 28:55:15 손가락연 6% 손연 38% 연철 1

200,0025      (etanoshdrcliufmwygvkpbjxqz) 78.64점 좌우 55:45 손가락 5:10:14:18-23:11:11:3 28:55:15 손가락연 6% 손연 40% 연철 1

 

1113,0230     (eatonrsdhculimfgpwvybkjxqz) 89.09점 좌우 55:45 손가락 8:9:13:18-17:17:13:1 27:57:14 손가락연 6% 손연 36% 연철 1

1113,0210     (eatonrsdhculimfgywvbpkjxqz) 89.06점 좌우 53:47 손가락 7:9:13:19-17:16:12:3 28:57:14 손가락연 6% 손연 37% 연철 1

1113,0211     (eatonrsdhculimfgywvkpbjxqz) 89.00점 좌우 53:47 손가락 7:9:13:19-17:16:12:3 27:57:14 손가락연 6% 손연 37% 연철 1

1113,0208     (eatonrsdhculimfgywvpbkjxqz) 88.06점 좌우 55:45 손가락 8:9:13:18-17:17:13:1 28:57:14 손가락연 6% 손연 36% 연철 1

 

중간 중간에 캡처를 떴는데 30점대부터 시작해서 점수가 점점 올라간다. 뒷부분부터 순회하기 때문에 초반 배치가 영 좋지 않다가 섞이기 시작하면 더 좋은 배열이 나타나는 모양이다. 최종적인 채점 결과는 다음과 같다.

 

5670,1397(aoethrnsdliufcmwgpbykvjxqz)

경과 시간 : 0:33:48, 초당 처리 개수 : 27959

 

3299,9491     (tenasohrlducimfgpwvykbjxqz) 97.82점 좌우 53:47 손가락 8:10:11:20-19:16:13:0 28:55:15 손가락연 5% 손연 33% 연철 0

3299,9489     (tenasohrlducimfgpwbykvjxqz) 97.71점 좌우 52:48 손가락 7:10:11:21-20:16:13:0 28:55:15 손가락연 5% 손연 33% 연철 0

3300,0423     (tenasohrldumicfgpwvykbjxqz) 97.69점 좌우 53:47 손가락 8:10:11:20-19:16:13:0 27:55:17 손가락연 5% 손연 33% 연철 0

3306,4045     (tenasohdlrucimfgpwvykbjxqz) 97.59점 좌우 53:47 손가락 8:10:11:20-19:16:13:0 25:55:18 손가락연 5% 손연 33% 연철 0

3300,0421     (tenasohrldumicfgpwbykvjxqz) 97.59점 좌우 52:48 손가락 7:10:11:21-20:16:13:0 27:55:17 손가락연 5% 손연 33% 연철 0

3306,4443     (tenasohdlrucifmgpwbykvjxqz) 97.48점 좌우 52:48 손가락 7:10:11:21-20:16:13:0 25:55:18 손가락연 5% 손연 33% 연철 0

3306,4043     (tenasohdlrucimfgpwbykvjxqz) 97.48점 좌우 52:48 손가락 7:10:11:21-20:16:13:0 25:55:18 손가락연 5% 손연 33% 연철 0

3299,9485     (tenasohrlducimfgpwyvkbjxqz) 97.30점 좌우 51:49 손가락 6:10:11:21-20:15:12:0 26:57:15 손가락연 5% 손연 33% 연철 0

3300,0415     (tenasohrldumicfgpwybkvjxqz) 97.21점 좌우 51:49 손가락 6:10:11:21-20:15:12:0 25:57:17 손가락연 5% 손연 33% 연철 0

3300,0417     (tenasohrldumicfgpwyvkbjxqz) 97.17점 좌우 51:49 손가락 6:10:11:21-20:15:12:0 25:57:17 손가락연 5% 손연 33% 연철 0

3299,9469     (tenasohrlducimfgywvpkbjxqz) 96.79점 좌우 53:47 손가락 8:9:11:20-19:16:13:0 29:55:15 손가락연 5% 손연 33% 연철 0

 

97점까지 점수가 나왔다. 1차 배치의 끝 부분은 빈도순인 jxqz로 통일시켰으며 이 부분은 순회 대상이 아니다.

 

1차 배치 :   etonislfdumarghcpkvwybjxqz

최고 점수 : tenasohrlducimfgpwvykbjxqz

 

수동 배치한 것과는 상당한 차이가 나는데 과연 어떤 배치가 더 나은지는 점수를 더 정밀히 내 봐야 알 수 있다. 아직 채점 기준이 명확하지 않고 변별력이 떨어지는데 이 부분은 더 보강해야 한다. 화면을 간소하게 정리하고 Best50개를 보이도록 했는데 이 개수는 더 늘릴 수도 있다.

 

여기까지 작업한 결과 놀라운 점은 5600만개를 채점하는데 불과 33분밖에 걸리지 않았다는 점이다. CPU를 저따구로 괴롭히니 빠를 수밖에 없다. 중간 예측인 12시간에 비해 20배 이상 빨라졌다. 이 계산대로라면 dist3133억개는 대략 5.3일이면 채점을 완료할 수 있다.

대신 소음, 발열이 심해 데스크탑에 함부로 돌리기는 좀 망설여진다. 저거 켜 놓고는 일을 하기 어렵고 안방이라 잘 때도 시끄러울 것 같다. 요가 노트북에서 실행해 보니 스레드를 5개밖에 쓰지 않으며 CPU 속도 0.4GHz에 점유율 20%로 초당 3000개밖에 채점하지 못했다. 배터리 모드라 그런가 보다 싶어 전원을 연결해 주니 3.3GHz로 풀 스피드 내며 점유율 100%로 초당 15000개씩 처리하다가 좀 지나니 바닥이 손을 못 델 정도로 뜨거워지며 2GHz로 감소하여 초당 1000개밖에 처리하지 못한다.

벤치상으로 41%의 차이가 나는데 실제로는 더 나는 것 같다. 용써 봐야 데스크탑의 절반밖에 안되니 역시 노트북이 데스크탑을 따라 잡기는 어려운 모양이다. 똑같이 i7-8000번대인데 코어가 4, 6개로 차이나고 뒤에 붙은 U가 저전력이라 제 속도를 내지 못한다. 더 빠른 데스크탑으로 업그레이드하면 더 많은 배열도 점검해 볼 수 있을 것 같아 현 시세를 좀 알아 봤다.

 

i7-8700 : 13079. 34만원

R7-4800H : 19231

R9-3900X : 32902. 55만원

TR-3990X : 80000. 396만원

i7-11700K : 25320. 38만원

i5-11600K : 19773. 25만원

 

젠장 11세대 i5보다도 점수가 더 떨어지네 그랴. 정 안되면 절반으로 나눠서 두 대 돌린 후 합치는 방법을 써야겠다. 본격적으로 돌려 보기 전에 채점 기준이 합당한지, 과연 고득점을 한 배열이 더 좋은지, 진짜 좋은 배열을 찾기 위해 뭘 중점적으로 봐야 하는지를 정비해야겠다. 이 기준을 제대로 정비하지 않고서는 돌려 봤자 헛수고가 될 것이다.

---------------------------

채점 결과를 저장하는 sEvaluate 구조체가 있고 그 중 고득점인 것만 sBest 구조체로 복사해서 관리하도록 했는데 생각해 보니 이 두 구조체를 따로 만들 필요가 없다. 그냥 sEvaluate 구조체의 사본을 배열로 가지고 있으면 된다. 처음에 이 둘을 따로 만들다 보니 일일이 멤버를 복사해야 하고 추가, 편집시 동기화해야 하는 불편함이 있었다. 멤버 이름을 싹 정리하고 arBest는 채점 결과의 사본을 100개 가지도록 했다. 뻔한 코드인데도 처음에는 그런 생각을 미처 하지 못해 리팩토링이 필요하다.

arBest는 최초 100개로 설정했다가 출력 시간이 오래 걸리고 화면도 좁아 50개만 표시했는데 그러다 보니 프로그램 종료 후 최종 결과를 확인할 방법이 없다. 그냥 100개를 다 출력하도록 하고 에디트를 화면 크기에 따라 늘리는 게 좋을 거 같다. 화면 출력 주기를 조정하는 다른 방법을 고안해 봐야겠다.

다음은 샘플을 조정한다. 임시적으로 7K 크기의 텍스트 샘플을 사용했는데 다양성이 좀 떨어지는 것 같다. 루프 도는 시간이 예상보다 오래 걸리지 않아 샘플이 좀 더 커도 될 것 같다. 링컨의 게티즈버그 연설문과 php 파일 하나 더 추가해서 대략 10K 크기로 맞췄다. 미국 사람들이 쓸 키보드이니 미국의 국민교육 헌장에 해당하는 텍스트를 사용하는 것도 큰 의미가 있다. 1차 조정 후 다음과 같이 빈도가 조정되었다.

 

샘플이 커져도 순위상으로는 큰 변화가 없다. 비율상으로는 조금 변화가 있으며 중반 이후부터 약간 달라진다. 이전에 웹에서 조사해서 지금까지 참조하던 비율과 실제 측정한 비율도 약간 차이가 있다.

 

7K일 때 :               ETOARINSHDCLPUYGMFWBVKXJQZ

10K :                      ETOARINSHDLCUPGMYFWBVKJXQZ

이전조사 :               ETAONSHRDLICUMFGWYPBVKJXQZ

 

10K 정도의 샘플이면 남들이 조사해 놓은 빈도와 큰 차이는 없고 절대적 순위가 없다. 샘플은 이 정도면 충분한 거 같으니 앞으로 계속 10K 샘플을 쓰기로 한다.

2153- 랭킹 최적화

직장에서 잘리는 바람에 갑자기 시간이 좀 많아졌다. 이번 기회에 루프 돌릴 수 있을만큼 프로그램 정비하고 최적의 배열을 찾아낼 생각이다. 기본 기능은 완료되었지만 아직도 UI가 어색하고 일시 정지/재개 정책이 명확하지 않아 프로그램을 좀 더 손을 봤다. 한번 통계만 뽑고 말게 아니라 계속 관리해야 할 프로그램이라 정성을 들여야 한다.

 

- 시작, 일시 정지, 재개를 하나의 버튼으로 합치고 프로그램 전체의 상태를 따로 관리한다. 정지하면 스레드 전체가 잠시 쉬도록 하여 CPU가 과열된 상태를 방지한다. 장시간 돌릴려면 꼭 필요하다.

- 상세 통계 보기 작성. 레이아웃을 복사하여 별도로 통계를 다시 낼 필요 없이 이미 조사한 sEvaluate 구조체를 출력하는 메서드를 따로 만들었다. , 이 기능은 정지중에만 사용할 수 있다.

- step으로 건너 뛰는 기능을 샘플링 기능으로 공식화했다. 백만개 중에 하나씩 건너뛰도록 하여 인접하지 않은 배열의 점수가 과연 합당하게 매겨지는지 점검할 수 있다.

- 결과 저장 및 읽기 기능 구현. 테스트중에 컴퓨터를 바꾼다거나 셧다운 후 계속 진행하려면 저장해 놓았다가 재시작할 수 있어야 한다. arBest 배열 전체를 저장하고 복구하도록 했다.

 

여기까지 작업한 후 좀 더 빠르게 배열을 점검하기 위해 노트북을 새로 하나 장만했다. 밤새도록 돌릴려면 시끄럽고 분리된 장소에서 장기간 돌리기 위해 빠른 노트북이 하나 더 필요하다. Ryzen7 4800H 모델이며 8코어 16스레드여서 현재 데스크탑보다는 확실히 빠르다.

데스크탑에 비해 근소하게 빠르기는 한데 모니터링해 보면 스레드 16개를 다 쓰지 않고 일부만 사용하는 현상을 보인다. 메인 스레드가 바쁘다 보니 작업 할당을 빨리 빨리 하지 못한다. 게다가 매 채점마다 스레드를 하나씩 할당하니 운영체제가 스레드 관리하느라 더 정신이 없다. 그래서 다음 작업을 더 진행했다.

 

- arEvaluate 작업 배열을 1000개 정도로 대폭 늘린다. 주는쪽과 받는쪽이 서로 기다리지 않도록 한다.

- 메인은 순열 순회하면서 arEv에 담당할 스레드 번호인 tid와 현재 순열을 복사해 놓고 작업 지시만 내린다.

- 각 작업 스레드는 생성시 자신의 번호인 tid를 받아 루프를 돈다. 종료하지 않고 계속 자기 일거리를 찾아 처리한다. PAUSE 신호에 대해 잠쉬 쉬기만 한다.

- 결과를 취합하는 별도의 스레드를 만들었다. 작업 스레드가 채점을 끝낸 것만 골라 arBest에 끼워 넣고 일정 주기마다 화면으로 출력한다. 메인의 부담을 덜어 줌으로써 최대한 빠르게 순회하고 멀티 CPU의 이점을 활용하기 위해서이다.

- 소스 수정 빈도를 줄이기 위해 변화를 줄만한 부분은 가급적 옵션으로 만든다. 스레드 개수를 최대 개수 -1, -4, 절반, 한개로 선택할 수 있도록 하고 런타임중에도 바꿀 수 있도록 했다.

- Pause 시간과 스레드 개수 변경 등에 대해 초당 처리 개수를 리셋하여 변경한 옵션의 결과를 확인할 수 있도록 했다.

- dist도 시작 전에 입력할 수 있다. 주로 2 아니면 3이며 4까지 갈 경우는 흔치 않을 거 같다.

- 출력 빈도를 동적으로 조정한다. 최초 만번, 십만번에 한번씩 출력하다가 백만 이후는 백만번에 한번 출력한다.

-손가락을 찾는 함수를 룩업 배열로 바꾸었다. 가급적 메모리를 많이 쓰고 속도를 높여야 한다.

- Shift의 부담도를 각 키에 할당하지 않고 따로 15로 정의했다. 대문자나 기호를 많이 누르면 부담도가 올라간다.

 

여기까지 작업한 후 소스도 1차 리팩토링했다. 변수명이나 함수명을 조정하고 부분별로 최적화를 수행했다. 곳곳에 디버깅 로그를 삽입하고 동기화 문제나 잔버그도 수정했다. 소스를 대폭 정리한 후 dist = 25600만개에 대해 각 옵션별로 처리 속도를 측정해 보았다.

 

옵션

속도

초당 처리수

최대 온도

i7-8700 T-1

1419

66079

87

i7-8700 T-4

1636

56990

86

i7-8700 T/2

1913

49230

83

i7-8700 1

9910

9539

61

4800H T-1

4737

20000

63. CPU 20%

4800H T-4

4257

20000

20%

4800H T/2

4644

20000

20%

4800H 1

1시간 4314

9164

10%

 

스레드를 더 많이 쓰면 확실히 처리 속도는 더 빨라진다. 대신 CPU 부하가 높아져 발열 및 소음이 심해지며 장비 내구성에도 문제가 있을 것 같다. 풀 스피드로 너무 맹렬하게 돌리지 말고 T-4 정도 옵션으로 돌리는게 적당할 것 같다.

새로 산 4800H는 어떨지 dist4 경우의 수를 세고 있는 중이라 천천히 돌려 봤는데 결과가 좀 이상하게 나온다. 멀티 스레드의 이점을 충분히 활용하지 못하고 오히려 더 느린데 아마도 main에서 Sleep을 주는 부분이 문제가 되는 것 같다.

소스를 여기 저기 찝쩍거리다 보니 혹시 통계가 좀 잘못된 부분이 없나 다시 한번 더 점검해 보았다. made in korea개행 이 샘플로 통계가 정확한지 측정해 보니 틀림없이 정확한 것 같다. 출연회수, 비율, 연타 계산 등을 수동으로 해 보고 일일이 대조해 봤는데 정확하다.

-------------------------

당장 필요치는 않지만 dist=4인 경우도 차후 점검해볼 필요가 있을 거 같아 일단 개수나 세어 보기로 했다. 이틀째 돌리고 있는데 현재 대략 4조개 가량이며 어림 잡아 20조개 정도가 아닐까 싶다. 애초에 콘솔로 만들다 보니 중간에 끊을 수 없고 한번 시작하면 계속 가야 한다. EnumLayout 예제를 다시 쓸 일은 없을지 모르겠지만 정리해 두기 위해 다음과 같이 수정했다.

 

- 만자리 출력 오류 해결. 나눈 후 % 연산을 해야 하는데 이걸 누락해 만자리에 억단위까지 나왔었다.

- 중간 결과에도 OK 수를 같이 출력하기. 이 값을 알아야 중간에 끊었다가도 다시 시작할 수 있다.

- 변수 초기값을 직접 지정하여 중간부터 셀 수 있도록 함.

- 최종 결과는 1자리까지 정확하게 64비트 정수로 출력했다.

 

여기까지 작업한 결과를 421일자 소스에 적용해 두었다. 노트북 2대를 동원해서 나머지 개수를 다 조사해 볼까 하다가 잘 생각해 보니 그럴 필요 없이 한 노트북에서 한꺼번에 돌려도 된다는 것을 깜박했다. 어차피 싱글 스레드 프로그램이고 CPU에 스레드가 남아 도니 세 개 정도는 동시에 돌려도 성능상 문제가 없고 오히려 전기와 시간을 아낄 수 있다. 앞으로도 대략 5일은 더 걸려야 dist 4까지의 정확한 개수를 알 수 있을 것 같다.

랭킹 프로그램도 이것 저것 손을 좀 봤다. 경과 시간과 초당 처리 개수를 스태틱에 출력하고 Shift 개수는 글자별 빈도로 옮겼다. 그리고 레이아웃의 배치를 한눈에 알아 보기 쉽도록 그래픽으로 통계를 출력했다.

키보드 레이아웃을 그려 배치를 보여 주고 행별 비율, 좌우 비율, 손가락 비율을 키 근처에 표시했다. 손가락 비율은 막대 그래프로 표시하여 대충의 비율을 파악하기 쉽도록 했다.

이 작업을 하다가 통계의 취약점 하나를 발견해서 수정했는데 손가락별 빈도가 좌우 따로이되 50% 기준으로 되어 있어 비직관적이다. 전체 손가락에 대한 비율로 표시하는게 맞나 잠시 고민하다가 각 손에 대한 백분율이 실용적인 것 같아 기존 논리를 그대로 취하되 백분율로 바꾸었다.

통계 출력이나 최적화는 웬만큼 된 것 같고 이제 점수가 제대로 뽑히는지 점검해 볼 차례이다. 막상 테스트해 보니 자잘한 수정에 의해 몇 가지 문제가 있었다. 손가락 비율을 백분율로 바꾸어 감점이 2배로 과대 평가되어 가중치를 0.5로 조정했다. 쉬프트에 대해서도 부담도를 증가시키다 보니 평균 부담율이 15.71로 상승하여 부담도 감점도 심해져 만점을 15.7로 조정했다.

이 상태에서 샘플링을 해 보니 음수 점수는 arBest의 디폴트 점수인 0점보다 작아 집계되지 않는 문제가 발견되었다. 그래서 arBest의 초기 점수를 -1000으로 지정했다. 또 샘플링이 끝난 후 STOP 상태가 되면 클릭해서 상세 통계를 볼 수 없다. END 상태를 추가하고 이 상태일 때는 상세 통계를 보여 주도록 했다.

100만개 단위로 샘플링해 보았다. 이 처리가 필요한 이유는 인접한 배열이 점수가 비슷해 다 상위권에 몰려서 랭크되므로 점수를 제대로 처리하고 있는지 알아 보기가 어렵기 때문이다. 건너뛰며 채점해 보고 제대로 평가하는지 점검해볼 필요가 있다. 결과는 다음과 같다.

 

     3300,0001     tenasohrlducifgmwypvkbjxqz 73.71점 부 15.73 50:50 14:17:22:44-00:00:40:29 27:56:16 지연 8% 손연 38% 연철 0

     3500,0001     taeonsrlhduicmfwgypkbvjxqz 56.28점 부 15.67 57:43 14:17:33:34-00:00:40:28 25:58:16 지연 8% 손연 45% 연철 0

     3000,0001     teoashnrlduicmwfgyvpbkjxqz 54.06점 부 15.69 55:45 16:15:27:40-00:00:42:27 29:54:15 지연 8% 손연 46% 연철 0

     4000,0001     toeanrhsidulmcgfpywkvbjxqz 54.06점 부 15.59 56:44 12:18:32:36-00:00:41:24 23:57:18 지연 9% 손연 45% 연철 0

      200,0001     etanoshdrcliufmwgpbykvjxqz 52.90점 부 15.71 55:45 13:19:27:38-00:00:49:22 27:55:16 지연 8% 손연 45% 연철 1

     2700,0001     teanorhsldciufmywgpbvkjxqz 50.92점 부 15.61 53:47 12:20:28:38-00:00:46:21 25:58:16 지연 9% 손연 46% 연철 0

     1400,0001     eatsonrdhlucigfmpbwykvjxqz 50.77점 부 15.62 57:43 17:19:26:36-00:00:38:29 26:57:16 지연 11% 손연 45% 연철 1

     1100,0001     eatonhdsrluicfgmpwvybkjxqz 49.00점 부 15.70 56:44 11:18:30:38-00:00:40:28 30:53:15 지연 10% 손연 43% 연철 8

     5400,0001     atesondhrlciufmgpwbykvjxqz 47.55점 부 15.75 55:45 12:20:34:31-00:00:48:20 28:54:17 지연 9% 손연 47% 연철 0

     4900,0001     aentoshridclufwmpgykvbjxqz 45.83점 부 15.72 50:50 13:22:23:40-00:00:47:26 25:56:18 지연 9% 손연 47% 연철 0

     1900,0001     eotanrhsldimcufywgbvpkjxqz 45.10점 부 15.59 52:48 12:18:26:43-00:00:39:25 22:57:19 지연 9% 손연 47% 연철 6

     3700,0001     taenosdrhcilmufwpgbyvkjxqz 43.43점 부 15.76 57:43 11:19:30:38-00:00:47:22 25:54:19 지연 9% 손연 46% 연철 0

     1000,0001     etnoarhsdiulcgfmywpkvbjxqz 42.06점 부 15.59 53:47 12:20:22:45-00:00:41:26 23:58:18 지연 9% 손연 47% 연철 5

     4100,0001     toeahsndrcliufgmywpvbkjxqz 41.38점 부 15.76 54:46 13:16:35:34-00:00:45:24 27:56:16 지연 8% 손연 49% 연철 3

     3900,0001     taesnodhrcluifmywgbpvkjxqz 39.36점 부 15.76 52:48 13:18:29:38-00:00:41:27 29:54:15 지연 10% 손연 46% 연철 13

     5600,0001     aoetsndrhiulcfmgypwkbvjxqz 39.16점 부 15.78 54:46 10:16:30:42-00:00:36:31 26:55:17 지연 10% 손연 50% 연철 0

     5200,0001     ateohnrsldimfcuygwvbpkjxqz 38.65점 부 15.64 53:47 16:17:31:33-00:00:44:22 22:56:21 지연 9% 손연 49% 연철 3

     5500,0001     aoetnsrhdlicmugyfpwbkvjxqz 38.20점 부 15.68 54:46 16:18:31:33-00:00:43:28 22:58:19 지연 10% 손연 49% 연철 0

     4500,0001     aetnohrsdiclfmugpwykbvjxqz 36.59점 부 15.64 55:45 15:21:27:36-00:00:48:21 22:57:19 지연 10% 손연 49% 연철 1

     3600,0001     taeoshndrilufcmwgpybkvjxqz 35.09점 부 15.79 57:43 14:15:30:39-00:00:46:24 22:56:21 지연 10% 손연 45% 연철 3

      900,0001     etnahosrdlumicgfwbpykvjxqz 33.78점 부 15.72 51:49 15:16:22:45-00:00:39:30 27:55:16 지연 10% 손연 46% 연철 13

     4300,0001     aetonhsdriulcmfwygpvkbjxqz 33.50점 부 15.75 52:48 14:18:28:37-00:00:48:25 24:56:18 지연 10% 손연 46% 연철 13

     2900,0001     teoansrhildcugmfywvkpbjxqz 33.37점 부 15.56 51:49 17:19:24:38-00:00:47:22 25:56:17 지연 11% 손연 45% 연철 14

     1600,0001     eaotsndhrcilugfmpwykbvjxqz 32.71점 부 15.73 49:51 11:18:26:43-00:00:44:26 25:55:19 지연 10% 손연 46% 연철 13

     2300,0001     eoatsrndhcluimfywgpkbvjxqz 32.61점 부 15.64 48:52 15:17:23:43-00:00:36:33 24:58:16 지연 10% 손연 48% 연철 4

     5100,0001     ateonrshdliufcmygwbkpvjxqz 32.04점 부 15.60 51:49 12:19:33:34-00:00:44:21 21:57:21 지연 10% 손연 50% 연철 3

     2500,0001     teaonrshildumcfgpwyvkbjxqz 31.43점 부 15.56 50:50 14:20:25:39-00:00:48:21 22:57:19 지연 10% 손연 46% 연철 14

     1300,0001     eatnorshlcduimfygwbkpvjxqz 31.21점 부 15.58 52:48 12:20:26:40-00:00:35:29 25:57:17 지연 11% 손연 48% 연철 18

     5300,0001     atenorsdhcilumwfygpbvkjxqz 29.64점 부 15.64 51:49 13:20:32:33-00:00:48:20 22:58:18 지연 11% 손연 45% 연철 16

     4400,0001     aetosrnlhcdmigufwybpvkjxqz 27.99점 부 15.60 48:52 18:19:27:34-00:00:42:28 25:57:16 지연 10% 손연 47% 연철 14

     3400,0001     tenoahsdrlcuimwfgybpvkjxqz 27.09점 부 15.69 48:52 16:22:21:39-00:00:45:29 28:55:16 지연 10% 손연 46% 연철 13

      600,0001     etoahnsdlrciumwgfbpyvkjxqz 26.77점 부 15.72 55:45 13:15:27:43-00:00:43:24 24:55:19 지연 11% 손연 47% 연철 14

     2000,0001     eotahsnrildumcgyfpwbkvjxqz 26.53점 부 15.75 57:43 14:15:27:42-00:00:46:24 24:56:18 지연 11% 손연 45% 연철 13

     3200,0001     teosahnlrduicgmyfwpbvkjxqz 26.52점 부 15.75 53:47 14:20:28:35-00:00:50:22 28:56:15 지연 10% 손연 48% 연철 13

     4200,0001     toesanhridlcfmuwgypvkbjxqz 26.42점 부 15.76 58:42 12:19:28:38-00:00:47:20 25:56:17 지연 9% 손연 50% 연철 0

     1500,0001     eaotnshdirclumgfwyvbpkjxqz 26.16점 부 15.68 53:47 12:18:23:45-00:00:39:29 25:54:19 지연 11% 손연 48% 연철 14

     2400,0001     eontahsldrucimfygwpkbvjxqz 25.39점 부 15.76 52:48 12:20:22:44-00:00:35:36 25:56:18 지연 10% 손연 48% 연철 16

     2600,0001     teaohndsrcilufmgwpbykvjxqz 23.69점 부 15.73 49:51 14:17:25:42-00:00:51:21 25:55:18 지연 11% 손연 45% 연철 13

     2100,0001     eotsanhrdcilufwmgybkpvjxqz 22.81점 부 15.72 56:44 11:19:26:42-00:00:44:20 25:55:19 지연 10% 손연 50% 연철 13

     1800,0001     eanthosrdcluimwyfgpvkbjxqz 21.42점 부 15.76 50:50 14:16:20:47-00:00:36:34 26:56:17 지연 11% 손연 47% 연철 16

      100,0001     etaosnrhldicmfugpbwyvkjxqz 21.09점 부 15.59 53:47 17:18:23:40-00:00:46:22 23:57:19 지연 11% 손연 47% 연철 17

     2200,0001     eoatnhsrildcmuwfygbpkvjxqz 20.99점 부 15.70 55:45 15:17:23:43-00:00:43:28 26:55:17 지연 10% 손연 50% 연철 13

     5000,0001     aentsordhiclfumwpgvybkjxqz 19.81점 부 15.62 50:50 18:18:24:38-00:00:42:26 23:57:19 지연 10% 손연 51% 연철 13

      500,0001     etoanhrsldiumcfywgvkpbjxqz 19.79점 부 15.62 54:46 16:17:24:41-00:00:45:23 21:57:21 지연 10% 손연 50% 연철 9

      300,0001     etansodhrlicfuwmgpbyvkjxqz 19.62점 부 15.74 51:49 13:17:25:43-00:00:49:19 25:55:19 지연 10% 손연 48% 연철 16

      700,0001     etonarshdliufcwmgpybvkjxqz 18.92점 부 15.61 53:47 13:20:24:41-00:00:47:20 20:58:20 지연 11% 손연 47% 연철 16

     4700,0001     aetshonlrdcimufgpwybkvjxqz 18.47점 부 15.75 52:48 16:17:34:32-00:00:50:18 26:55:18 지연 11% 손연 48% 연철 16

     2800,0001     teasondrhilcfmuypgwvbkjxqz 17.62점 부 15.74 55:45 10:21:22:44-00:00:48:18 25:55:19 지연 12% 손연 47% 연철 16

      400,0001     etasnohrldcuimwfygpkvbjxqz 17.51점 부 15.75 52:48 13:18:21:46-00:00:41:27 25:56:17 지연 11% 손연 50% 연철 14

     4600,0001     aetsonhdrcliumwgfpyvkbjxqz 17.46점 부 15.75 53:47 13:19:33:33-00:00:51:19 26:56:17 지연 12% 손연 48% 연철 16

             1 etaonshrdlicumfgwypbvkjxqz 16.91점 부 15.71 53:47 13:18:23:44-00:00:48:23 24:56:18 지연 11% 손연 48% 연철 16

     1700,0001     eantosrhlidmfcugpwvybkjxqz 15.83점 부 15.62 57:43 16:20:20:42-00:00:38:29 21:56:21 지연 10% 손연 50% 연철 13

     1200,0001     eatohnsldrcifmuwygpvbkjxqz 14.28점 부 15.77 57:43 11:16:30:41-00:00:41:24 24:56:19 지연 10% 손연 50% 연철 16

     3100,0001     teonasdhlriumcfwgbpyvkjxqz 11.29점 부 15.77 53:47 12:20:24:42-00:00:51:20 20:55:23 지연 11% 손연 48% 연철 13

     3800,0001     taenhorsdilumcgfwpvykbjxqz 10.34점 부 15.67 59:41 17:14:28:39-00:00:40:23 21:57:21 지연 10% 손연 49% 연철 3

     4800,0001     aeotsnhlirducmwgfypkbvjxqz 8.59점 부 15.74 46:54 13:18:24:42-00:00:45:27 23:56:20 지연 11% 손연 49% 연철 1

      800,0001     etnaosrhldciumfwgpyvkbjxqz 2.49점 부 15.65 57:43 16:19:24:39-00:00:45:25 23:58:17 지연 11% 손연 51% 연철 19

 

점수 분포는 최하 2점에서 최고 73점까지로 골고루 분포되어 있으며 음수도 나올 수 있지만 샘플링 예에는 없다. 레이아웃 ID도 비교적 잘 분산되어 있어 배열이 바뀌면 점수가 확연히 달라짐을 알 수 있다.

부담도는 빈도순에서 앞뒤로 두칸씩만 움직이므로 크게 악화되지 않고 모두 15점대를 유지한다. 가장 낮은 부담도를 받은 레이아웃이 15.61인데 이게 어떻게 빈도순보다 더 낮게 나올 수 있는지 이상하다. 혹시 채점 논리가 잘못된 게 아닌지 살펴보았다.

 

 

빈도순 :       etaonshrdlicumfgwypbvkjxqz   총부담도 : 165041, 평균 : 15.71

15.61:     teanorhsldciufmywgpbvkjxqz  총부담도 : 164429, 평균 : 15.61

 

일단 빈도순에서 dist=2 거리 내에서만 움직였음은 확인했다. 다른 배열도 2 초과 거리로 움직인 예가 없어 순회는 잘 되고 있다. 두 배열은 순서가 조금 다른데 각 부분의 부담도를 비교해 보자.

 

eta, tea : 이 둘은 결과가 같다. et위치의 부담도가 같아 바꿔도 변화 없다.

on, no : 빈도순의 o11, n12인데 이 둘이 바뀌었다. 그러나 출현 빈도는 o가 더 높아 on을 바꾸면 부담도는 더 증가한다.

shr, rhs : r의 위치가 바뀌었다. 빈도순의 r은 우선순위 7번이며 부담도 20이다. 반면 r이 앞으로 이동하면 우선순위 5번이며 부담도 12이다. r이 좋은 위치로 이동하여 부담도가 덜하다. sh가 더 낮은 자리로 이동하여 부담도가 높아지지만 출현 빈도상 rsh보다 더 높아 이득을 보게 된다.

 

빈도순은 웹에서 조사한 빈도를 따른 것이고 샘플의 빈도와는 다르다. 샘플에 더 자주 등장하는 문자가 더 좋은 자리로 이동하면 부담도는 떨어지게 된다. 그렇다면 샘플의 빈도대로 배치하면 부담도가 가장 낮을 것이다. 과연 그런지 테스트해 보았다.

 

샘플상 빈도 :          etoarinshdlcupgmyfwbvkxjqz

기존 빈도순 :           etaonshrdlicumfgwypbvkjxqz

1차 배치 :               etonislfdumarghcpkvwybjxqz

 

빈도 높은 문자별에 좋은 자리를 배정하면 부담율은 15.47이며 이 값이 가장 낮은 부담율이다. 기존에 비해 ip의 빈도가 대폭 높아졌다. 샘플의 영향을 많이 받는 셈이다. 샘플상 빈도를 시작점으로 하면 어떨까 싶지만 샘플이 워낙 가변적이라 큰 의미는 없을 것 같다. 대신 좀 더 공인된 빈도를 구해야 할 것 같다.

1차 배치와 빈도순 배열과의 관계도 살펴 보았다. 모음끼리 비슷한 자리에 모으기 위해 a는 너무 뒤쪽에 있고 i는 너무 앞쪽에 있다. h가 약간 뒤쪽인데 많이 쓰는 철자인 wh를 붙여 놓기 위해서였던 것 같다. 이 예를 보면 꼭 빈도순으로만 배치하는 것이 이상적이지는 않으며 다른 이유가 개입할 여지가 많음을 알 수 있다. 채점 요인이 더 많아져야 하고 정밀도도 높여야 한다.

좌우 분담률은 48~57까지로 이상적값인 53에서 멀지 않다. 손가락 분담률도 표준에서 멀지는 않은데 출력문에서 오른손 집게, 중지가 0으로 나타난다. FingerRate 배열의 첨자 의미를 중간에 바꾸었는데 집계에 적용하지 않아서 발생한 버그이다. 즉시 수정했지만 이래서 변수 의미를 자꾸 바꾸어서는 안되며 소스 안정성이 필요하다.

손가락 연타율은 대략 10% 안팎이고 손연타율은 45~50사이이다. 연철은 아예 없는 것도 있고 최대 19점이나 깍인 경우도 있다. 조사해 보니 eh, in, de 연철이 과연 있으며 0점인 레이아웃은 진짜 연철이 하나도 없다. 연철 조사를 제대로 하고 있다. 그 외 Best 클릭시 그래픽 출력을 위해 화면 전체가 깜박이는데 무효 영역을 설정하여 깜박임을 제거했다.

샘플링 기능은 프로그램이 잘 돌아가고 있는지, 레이아웃에 따라 점수를 잘 산출하는지 점검하기 위해 만든 것이다. 테스트해 보니 순회나 채점, 집계는 정확한 것 같다. 이제 점수의 변별력을 높이는 처리가 필요하다.

그 전에 영문 빈도부터 다시 조사할 필요성을 느꼈다. 웹에서 검색한 것은 대소문자를 따로 구분하거나 기호까지 포함하고 있어 자판 배열을 결정하는 빈도로는 적합하지 않다. 그동안 사용했던 빈도는 웹에서 검색한 것인데 적합치 않다. 결국 대규모의 샘플을 통해 빈도를 정확히 조사할 필요가 있다.

-------------------------------------

레이아웃 채점은 코드를 좀 더 완성한 후 돌리더라도 개수부터 세어 보자 싶어 t4, u4, v4에 대해 루프를 돌렸다. v4는 저번주부터 돌리기 시작했고 멀티 CPU의 이점을 활용하기 위해 셋 다 한꺼번에 돌리기 시작했다. 그리고 사흘간 휴가를 다녀 왔는데 t4는 끝냈고 u4, v4는 아직도 실행중이었다.

그런데 맙소사 t4가 끝은 났지만 창이 닫혀 버려 결과를 알 수 없었다. 더블클릭해서 바로 실행하다 보니 순회만 하고 종료해 버리는 것이다. 이런 식이면 v4도 일주일째 돌리고 있지만 결과를 알 수 없다는 얘기다. 어쩔 수 없이 t4는 명령행을 열어 다시 실행중이고 v413조까지 세다가 중간에 끊었다.

마지막 줄에 getch 하나만 넣었어도 되는데 이걸 몰라 다시 해야 하다니 억울하다. 이왕 이렇게 된 김에 좀 더 최적화를 해 보자 싶어 코드를 다시 봤더니 취약한 점이 보인다. 원래 TestInDist 메서드는 다음과 같다.

 

bool TestInDist()

{

    for (int idx = 0; idx < last + 1; idx++) {

        // 원래 문자가 현재 배열의 어디쯤에 있는지 조사

        int off = strchr(ar, ori[idx]) - ar;

        if (ABS(off - idx) > dist) return false;

    }

    return true;

}

 

순회중인 ar 배열에서 원래 문자가 어디쯤으로 가 있는지 조사한다. strchr 함수로 원래 문자의 번지를 찾고 ar 선두 위치를 빼면 오프셋이 나오며 이 오프셋의 절대값을 취해 이동 거리를 판별한다. 이럴 필요 없이 두 배열의 대응 문자끼리 바로 빼 버리면 이동 거리를 알 수 있다. 수정된 코드는 다음과 같다.

 

inline bool TestInDist()

{

    for (int idx = 0; idx <= last; idx++) {

        // 대응 위치의 문자끼리 빼서 dist 이상 거리인지 조사.

        if (ABS(ar[idx] - ori[idx]) > dist) return false;

    }

    return true;

}

 

아래위로 문자 코드끼리 빼서 절대값을 취하고 이 거리가 dist를 초과하면 false이고 모든 문자가 dist 내이면 true이다. strchr 함수 호출과 포인터 연산이 빠져 속도가 더 빨라진다. < last + 1<= last로 바꾸고 inline 지정을 추가하여 가급적 속도에 유리하도록 조정했다. TestInDist(int)도 같은 방식으로 수정하되 함수로 만들 필요도 없이 수식 하나로 바로 점검 가능하다.

 

    // i위치로 교환한 문자가 범위안일 때만 reverse 실행, 아니면 건너뜀

    if (ABS(ar[i] - ori[i]) <= dist) {

        // 뒷부분 오름차순으로 정렬

        for (j = last, i++; i < j; i++, j--) {

            SWAP(ar[i], ar[j], t);

        }

    }

 

i위치의 문자가 원래 문자 위치에서 dist 이내 거리일 때만 reverse를 실행하도록 했다. 함수를 호출하지도 않고 수식으로 바로 조건을 점검할 수 있으니 이게 당연히 더 빠르다. 결과 출력도 속도가 더 빨라졌으니 5000만번에 한번으로 조정했다.

 

q4 : 원래 15-> 923초로 단축

p4 : 원래 3-> 150초로 단축

 

물론 개수는 똑같이 조사된다. 잘 연구해 보면 확실히 더 빠르고 좋은 방법이 있다는 것을 알 수 있다. 혹시나 해서 getch 호출문도 끝에 넣었다. 개수만 세고 나면 더 쓸 일은 없지만 그래도 정리를 잘 해 둘 필요가 있다.

소프트웨어를 잘 만들면 하드웨어의 부족한 성능을 메꿀 수 있는 방법이 많다. 한번 쓰고 말 프로그램이라고 너무 대충 만든 것 같다. 애초에 콘솔로 만든 것부터 좀 실수였다. 이래서 채점 프로그램에 대한 정밀한 튜닝이 필요하다.

-------------------------

dist=2에 대해 4800H에서의 실행 결과가 오히려 더 느리게 나와 좀 당황스럽다. 스레드는 다 사용하는데 CPU 점유율은 오히려 낮아 어디선가 병목이 생긴 것 같은데 아무래도 Sleep이 문제인 것 같다. 또한 메인창이 너무 커 노트북에서 테스트해 보기 어려운 면이 있어 다음 사항을 개선했다.

 

- 결과 에디트에 WS_HSCROLL 속성을 주어 개행하지 않도록 함

- 메인창의 크기를 화면 크기에 따라 조정. FHD에서는 1400 폭에 맞춤

- TestInDist 수정한 것 적용

- main, Ev, AggSleep 옵션 조정

- Best 출력시 arBest[0]의 상세 통계도 출력

 

여기까지 작업한 후 테스트해 보니 여러 가지 문제가 더 발생했다. 채점 배열이 1000개나 되는데 특정 스레드가 한번에 하나밖에 처리하지 못하는 경우가 있고 손가락 연타율이 -2147483648로 나오기도 한다. 뭔가 스레드간에 꼬이는게 있다는 얘기다. Sleep을 적당히 주면 이런 현상은 나타나지 않는데 맹렬히 돌릴 때는 동기화 문제가 있는 것 같다. 논리적인 문제를 발견했으니 이 문제를 해결한 후에야 통계를 돌려볼 수 있을 듯 하다. 내일부터 출근해야 하니 이 문제는 천천히 좀 더 궁리해 보기로 한다.

-------------------------

아무리 빨라도 결과가 틀리면 아무짝에도 쓸모없는 통계이다. 디버깅을 좀 더 해 보니 총 타수가 10506이 아닌 배열이 발견되었다. 똑같은 샘플로 조사하는데 타수가 다를 수가 없다. 최소 17타로 끝나는 경우도 있는데 그러다 보니 손가락 연타수가 0이고 비율은 무한대가 되는 경우까지 발생했다.

디버깅해보니 7번 스레드가 특정 배열을 채점하고 있는데 4번 스레드가 똑같은 배열을 채점하는 현상이 발생했다. 메인에서 원래 tid7을 대입했는데 어떤 이유로 작업이 끝나기도 전에 4로 다시 바꾼 것이다. 동기화의 문제인데 status가 문제인 것 같다. 이 값을 EVAWAITAGGWAIT로 변경하는 스레드가 중간에서 잘리면 status가 엉뚱한 값을 가지게 되며 tid도 마찬가지이다.

arEvaluate 배열을 세 스레드가 동시에 참고하는데 읽을 때나 쓸 때 동기화를 하지 않으니 서로 엉뚱한 값을 보게 된다. Sleep이 있을 때는 그나마 쉬어가는 타임에 스위칭이 발생해 이런 문제가 거의 발생하지 않지만 맹렬하게 돌리면 그럴 수도 있다. 그래서 뮤텍스로 일일이 막아서 동기화를 해 봤는데 깨지는 것은 방지할 수 있지만 속도는 형편없이 느려졌다.

충돌을 방지하려면 각 스레드별로 하나씩 채점하고 집계까지 해야 한다. 애초의 방식과 비슷하되 집계는 굳이 스레드를 만들지 않고 채점 후 집계하도록 했다. main에서 sEvaluate를 할당해서 스레드로 던지고 스레드는 이 배열을 채점 및 집계하고 조용히 사라지니 충돌이 발생할 여지가 없다. 과연 말썽은 없는데 최대 2만개 이상의 속도는 나지 않는다.

스레드를 늘리면 어떨까 싶어 최대 스레드의 2배수까지 늘려 봤는데 그래도 별반 차이는 없으며 CPU 활용율도 떨어졌다. 스레드는 커널 객체라 만들고 파괴하는데 엄청난 시간이 들기 때문이다. 그래서 스레드는 그대로 두고 작업 거리만 전달하는 방법을 연구하기 시작했는데 그게 스레드 풀링이다. 전에 써 본적이 없던거라 생소한데 회사에서 좀 연구해 봤더니 아예 처음 보는 문법이었다. 이걸 좀 연구해 볼까 하다가 너무 시간이 많이 들 것 같고 해서 대충 개념만 익혔다.

필요한 건 스레드를 매번 만들지 않고도 작업거리를 던져줄 수만 있으면 된다. 그래서 스레드별로 구조체를 만들고 여기다 작업거리를 넣어준 후 이벤트로 통보하는 방식을 고안했다. 작업 시작, 작업중임을 수동 이벤트로 처리하고 메인과 작업 스레드는 이벤트 상태에 따라 대기하면 된다. 여기서 한발 더 나아가 한번에 하나씩 처리하지 말고 묶음으로 처리하면 대기가 짧아 더 빠를 것 같았다.

이를 위해 메인과 작업 스레드의 분배 속도를 계산해 봤다. 메인은 초당 5000만번 순회하는데 이중 유효 거리 내의 배열은 대략 100개 중에 1개이니 초당 50만개의 후보를 골라내는 셈이다. 스레드는 채점을 얼마나 빨리 하는지 속도 테스트 코드를 작성하여 조사해 봤다. 디버그 버전에서는 초당 4500, 릴리즈는 초당 10500개 정도를 채점한다. 집계까지 감안하면 초당 5000개 정도 가능하다는 얘기고 10개 스레드를 돌리면 5만개 정도 속도가 나와야 정상이다.

메인의 순회 속도는 이보다 월등히 빠르니 Sleep만 안 하면 스레드에게 작업거리는 충분히 제공해줄 수 있다. 스레드는 배열 하나를 1/5000초만에 채점하는데 이거 채점하고 이벤트를 또 기다리다가는 시간 다 까 먹게 된다. 그래서 한번에 1개가 아니라 묶음으로 여러 개를 던지기로 했다. 묶음으로 처리하면 대기 시간이 줄어들고 집계도 한꺼번에 할 수 있어 유리하다. 메모리는 얼마든지 넘치니 묶음이 좀 커도 상관 없다.

이 방식으로 코드 흐름을 설계해서 대략 1시간만에 코딩을 마쳤는데 한방에 성공했다. 역시 이론 연구가 확실하면 구현은 쉬운 편이다. 묶음 크기에 따른 속도를 측정해 봤는데 결과는 다음과 같다. 디버그 버전에서의 측정이다.

 

1: 초당 4000

2: 초당 10000

5: 초당 18000

10: 초당 20000, CPU 60%

50: 초당 24000. CPU 78%

100: 초당 26000. CPU 82%

 

채점 속도 측정한 것대로 나온다. 묶음 1이면 스레드를 암만 돌려 봐도 한번에 하나씩 하는 것과 같고 묶음 2이면 거의 2배 속도가 나온다. 10개 이상은 큰 차이가 없는데 어차피 스레드 개수로 속도를 조정할 수 있으니 그냥 100개로 고정했다. 스레드 개수와 시스템에 따른 속도는 다음과 같다.

 

 

데탑 i7-8700

노트북. 4800H

회사 3550H

T/2

6스레드. 4.8, 60%

8스레드. 4.850%

 

T-4

8스레드. 6, 80%

12스레드. 7.380%

4스레드. 2.1100%. 3.2GHz

T-1

11스레드. 7.7100%

15스레드. 8.898%

7스레드. 3. 100%. 3.2GHz

 

스레드 개수에 따라 속도차가 정직하게 나온다. 8.8만으로 계속 돌리면 발열에도 좋지 않을 거 같으니 7만 수준으로 돌리는게 나을 것 같다. 데스크탑에서 dist-2. 5600만개를 돌려 보았다.

데이터 깨지는건 없고 16분만에 초당 5.7만개로 작업을 끝냈다. 노트북에서 돌리면 이보다 20%는 더 빨리 끝날 것 같다. 초당 7.3만이면 하루에 63억개를 채점할 수 있다는 얘기이며 dist-3133억개는 불과 이틀이면 채점이 끝난다는 얘기이다.

dist-4는 아직 개수 파악중인데 직접 실행하는 바람에 중간에 끊었다가 다시 시작해 사흘간 돌렸는데 윈도우 업데이트로 강제 재부팅되는 바람에 다시 돌리고 있다. 대략 1.5조개 정도 될 거 같은데 계산상 238일이 걸리는 셈이다. 이거 돌리려면 좀 더 최적화를 하거나 더 빠른 컴퓨터를 구하거나 여러 대를 돌려야 할 판이다.

-----------------------------------

스레드 구조를 만들었으니 이제 채점 속도를 더 최적화해 보기로 한다. 아직도 메모리를 좀 더 쓰고 사전 준비를 한 후 돌리면 최적화할 여지가 많다. 현재 디버그 기준으로 초당 4600번 채점하는데 구두쇠짓을 좀 더 해 보기로 한다.

 

- 통계 초기화를 위해 개별 변수에 일일이 0을 대입하지 말고 memset으로 한꺼번에 초기화했다. LastFinger, LastHand는 원래 -1로 초기화했는데 손가락, 손번호를 1부터 시작하도록 조정하여 그냥 0으로 초기화했다. TotalTime0으로 초기화하는 코드도 필요 없다. 대입문을 상당히 줄였지만 여전히 4600개로 별 변화 없다. 역시 문장 몇 개 빼는 건 별 의미 없고 루프를 줄여야 한다.

- jxqz는 변하지 않으므로 키 정보를 CharInfo에 미리 조사해 대입해 두면 LAST까지만 루프를 돌면 된다. 루프 횟수를 약간 줄일 수 있을 거 같았는데 쿼티나 알파벳순 등 뒷부분도 바뀌는 배열이 있어 쓸 수 없었다. 또 문자가 레이아웃에 없는 에러 처리는 순회중에는 일어나지 않으므로 디버그 버전에서만 점검하도록 했다.

- Preprocess 함수를 추가하고 배열과 상관없는 총타수, Shift 횟수, 문자별 등장 회수는 미리 조사해 두었다. 통계를 낼 때는 각 문자를 순회하며 문자별 부담도에 횟수를 곱해 좀 더 빠르게 계산해 낸다. 손가락 누른 횟수나 행별 타이핑 수도 곱하면 바로 구할 수 있다. 이 처리에 의해 초당 5600개로 대략 20% 정도 빨라졌다.

- 총타수나 키 누른 횟수는 미리 구해둘 수 있지만 손연타, 손가락 연타는 레이아웃에 영향을 받으니 샘플 문자열을 순회하는 수밖에 다른 도리가 없다. 이 루프가 좀 긴데 얼마나 영향을 받는지 샘플 길이를 조정해 봤다.

 

11K : 5600

5K : 12000

2K : 30000

 

샘플이 줄어들면 연타 계산 루프의 반복 횟수가 줄어들어 속도가 대폭 향상된다. 2K인 상태에서 릴리즈 T-4로 돌려 보니 초당 23만개를 채점해 내니 딱 샘플 길이에 반비례하는 관계이다. 차후 샘플을 좀 더 줄인 후 테스트해야겠다.

- 샘플을 줄이는 건 당장 할 일은 아니어서 현재 상태에서 가능한 최적화를 더 찾아 봤다. 샘플 루프를 돌때 유요한 문자인지 점검하고 소문자로 바꾼 후 문자의 정보 인덱스를 찾는데 이 과정을 Preprocess에서 미리 해 둔다. sample_idx 정수형 배열을 만들고 각 문자의 인덱스만 저장해 둔다.

본 루프에서는 이 인덱스로부터 손가락 정보만 읽으면 된다. 문자열 포인터 연산이 아닌 정수 루프라 더 빨라지는데 14000개로 처리량이 2배 이상 늘었다. 기대하지 않았던건데 효과가 좋아 만족스럽다. 연타 개수를 if문으로 점검하는 코드도 bool값을 바로 더하는 코드로 바꿔 보았다.

 

pEv->FingerOver += (tFinger == LastFinger);

 

이건 오히려 if문보다 더 느렸다. 처리 개수가 11000개로 주저 앉아 버려 원상 복구했다. 일단 평가한 후 0이든 1이든 무조건 더해서 그런 것 같다. 다음은 FingerCol의 개념을 구분했다. Col0부터 13까지 컬럼 번호이고 Finger는 각 손가락의 번호이며 왼손 1,2,3,4 오른손 9,10,11,12로 맵핑된다. 원래 코드는 열에 대한 손가락 번호를 루프 내에서 직접 찾았다.

 

Finger = pEv->arCharInfo[sample_idx[idx]].Finger;

tFinger = MainFinger[Finger];

 

룩업 테이블을 한번 더 거쳐 손가락 번호를 알아 내는 것도 시간이 걸릴 수 있다. 그래서 CharInfoColFinger 멤버를 추가하고 문자별 루프를 돌 때 Col로부터 Finger를 찾도록 했다. 문자 루프는 고작 26번 돌기 때문에 별 부담이 없다. 대신 알파벳이 아닌 문자는 수동으로 손가락 번호를 찾아 놓아야 한다. 이 간단한 최적화로도 16500으로 속도가 개선되었다. 다음 순회 루프를 개선해 보았다.

 

        for (int idx = 0; sample_idx[idx] != -1;idx++) {

                    Finger = pEv->arCharInfo[sample_idx[idx]].Finger;

 

첨자로 배열을 돌며 -1을 만날 때까지 통계 대상 문자를 구한다. 배열 액세스보다는 포인터가 더 빠르지 않을까 하여 다음 코드로 바꿔 봤다.

 

        for (int *pi = sample_idx; *pi != -1;pi++) {

                    Finger = pEv->arCharInfo[*pi].Finger;

 

왠지 포인터가 더 빠를 것 같은데 실측해 보니 아무 효과가 없었다. 원래의 쉬운 코드로 다시 원복했다. 손가락 번호를 CharInfo에 미리 조사해 두었으니 FindCharFinger 메서드는 더 이상 필요 없다. 문자 정보에서 바로 읽도록 하고 여러번 찾는 건 변수로 미리 조사했는데 이런 처리는 단발적이어서 속도 향상에는 별 도움이 되지 않았다.

Finger를 미리 조사해 놓는 방식이 효과가 있으니 Hand도 미리 조사해 놓으면 더 빨라질 것 같다. 그래서 CharInfoHand 멤버를 추가하고 문자 루프를 돌 때 미리 어떤 손인지 조사해 두었다. 루프 내에서는 바로 읽어들인다.

 

Hand = pEv->arCharInfo[sample_idx[idx]].Hand;

 

조건문 하나가 생략되었으니 더 빨라질 것 같은데 15000회로 오히려 느려졌다. 복잡한 참조문이 두 번 반복되어 그런가 싶어 CharInfo 구조체 포인터를 구한 후 포인터로 읽었다.

 

        sCharInfo* pChar;

        for (int idx = 0; sample_idx[idx] != -1;idx++) {

                    pChar = &pEv->arCharInfo[sample_idx[idx]];

                    Finger = pChar->Finger;

                   ....

                   Hand = pChar->Hand;

 

이러면 좀 빨라져야 하는데 14000회로 더 줄어든다. 포인터 대입, 참조가 별로 빠르지 않은 모양이다. 원래 조건문으로 원복하니 제 속도가 나온다. 이번에는 if문 대신 삼항 조건 연산자로 바꿔 보았다.

 

Hand = Finger < 7 ? 1:2;

 

문법상 똑같은 문장인데 이건 15000회로 더 느려졌다. 어쩔 수 없이 원래의 if문으로 다시 복귀했다.

 

if (Finger < 7) Hand = 1; else Hand = 2;

 

이 단순한 if문이 가장 빠르다. 왜 이런 차이가 발생하는지는 어셈블리 수준까지 내려 가서 점검해 봐야 알 수 있을 것 같다. Hand를 미리 조사하는 코드는 사용하지 않지만 다음에 쓸 수도 있어 그냥 두었다.

다음은 최후 손가락이나 최후 손번호를 대입하는 문장을 else 절 안에 넣어 보았다. 연타라면 굳이 최후 손가락을 갱신할 필요는 없는 것이 논리적으로는 맞다.

 

if (Finger == LastFinger) {

        pEv->FingerOver++;

} else {

        LastFinger = Finger;

}

 

그러나 예상했던대로 이게 더 느리다. 1650015300으로 내려갔다. 뭔가 조건을 판단하는 것보다는 같은 값일지라도 무조건 대입해 버리는 것이 가장 빠르다.

다음은 손가락과 손번호를 커다란 배열에 조사하면서 개수를 세 보았다. Last 변수가 필요 없고 이전값과 바로 비교할 수 있는데 참조문이 길고 어차피 비교는 해야 되므로 별다른 기대는 하지 않았다.

 

if (sam_fin[idx] == sam_fin[idx-1]) pEv->FingerOver++;

if (sam_hand[idx] == sam_hand[idx - 1]) pEv->HandOver++;

 

예상했던대로 13300회 정도로 속도는 오히려 떨어졌다. 메모리 할당 부담이야 별거 아닌데 논리만 간단할 뿐 절차가 복잡해 별 도움이 안되어 원상 복구했다. 안될거 뻔히 알면서도 ㅈ조금이라도 속도를 높이기 위해 별별 시도를 다 해 보고 있다.

문제의 핵심은 샘플 각 문자의 정보 인덱스는 미리 구해 놓을 수 있지만 손가락 번호를 미리 구해 놓을 수는 없다는 점이다. 레이아웃마다 배치가 달라지니 어쩔 수 없이 샘플 한 문자씩 순회하며 연타를 일일이 계산하는 수밖에 도리가 없다.

그러다가 한가지 기가 막힌 아이디어가 떠 올랐다. 연타는 횟수가 중요한게 아니라 결국 비율을 사용한다. 따라서 샘플 전체를 꼭 다 순회하며 점검할 필요는 없고 일부만 점검해도 근사한 연타율을 얻을 수 있다. 과연 그런지, 정확도의 희생에 따른 속도 향상은 얼마나 되는지 쿼티 배열로 점검해 봤다.

 

 

100%

80%

60%

40%

20%

예전

손가락연타

7.96

7.39

8.10

8.61

6.85

7.73

손연타

44.34

44.11

44.37

45.51

43.80

43.06

초당회수

16700

19800

28000

46800

128000

4600

빈도손연

12.09

11.69

12.24

12.75

12.14

11.75

1차손연

6.17

5.45

6.10

6.47

4.37

6.00

 

손가락 연타율은 대략 앞뒤로 0.6%씩 차이가 발생하는데 꼭 줄거나 느는게 아니라 왔다리 갔다리이다. 20% 샘플에서는 1%이상 차이가 발생해 너무 많이 줄이는 것은 바람직하지 않은 것 같다. 이 정도면 샘플 길이에 따라 꽤 차이가 많이 발생하는 셈인데 샘플이 어떤 텍스트이냐에 대해서도 영향을 많이 받는다. 현재 샘플은 텍스트와 코드가 중간 중간에 섞여 있는데 비율에 따라 코드가 얼마나 들어가는가에 따라 차이가 나는 것 같다. 좀 더 골고루 섞으면 차이가 덜해질 것이다.

손 연타율은 44%에서 앞뒤로 1% 이상 차이가 발생하지 않아 샘플을 줄여도 정확도 손실이 별로 없다. 속도는 샘플 길이에 반비례해서 늘어난다. 40%로 길이를 줄이면 이전보다 10배 더 빨라지는 셈이다. 해당 옵션을 선택하는 라이도 버튼 다섯 개를 배치하고 시작 전에 선택할 수 있도록 했다.

이 기능을 만들면서 한가지 치명적인 통계상의 문제를 발견했다. 100%로 했는데도 예전 통계와 차이가 발생하는데 디버깅해 보니 예전 총 타수는 10506인데 비해 100% 샘플을 다 사용해도 10204개밖에 안된다. 그 차이는 정확하게 Shift를 누른 횟수만큼인데 Shift가 과연 타수에 포함되는 것이 옳은지 다시 생각해 봐야겠다.

연타 점검시에는 대소문자 구분없이 연타를 찾아야 하므로 Shift와는 상관이 없는게 맞다. 그러나 손가락 분담률이나 행분담률을 계산할 때는 Shift도 포함해야 한다. Shift는 왼손 엄지 및 4행에 있어 이걸 계산하지 않으면 왼손 엄지는 노는 것으로 판단해 버리며 4행 비율도 낮아진다.

TotalTime에는 Shift를 포함시키고 부담도는 15 정도로 더했는데 통계에는 별 문제가 없는 것 같다. 좌우, 손가락, 행 분담률은 모두 엄지를 빼고 평가하기 때문에 Shift를 더하나 빼나 똑같은 비율이 나온다. Shift 포함은 현재 상태로 두고 연타율 계산에는 빼기로 한다. 현재 속도는 샘플 비율 100%로 해서 16500, 60%로 해서 28000개 정도이다.

연타율을 계산하는 코드 이후에도 각 분담율과 감점 계산, 총점 계산 등의 코드가 길게 있지만 모두 단순 산수라 속도에 별 영향을 주지 않는다. 이하의 채점 코드를 모두 주석 처리하고 측정해 봤자 초당 처리 개수는 기껏 100개 정도 늘어나는 정도에 그쳐 더 최적화할 필요는 없다. 이후 코드 리펙토링중에 보이는 정도만 최적화하기로 한다.

이제 릴리즈 모드에서 초당 12만개를 거뜬히 찍는다. dist2730초에 채점 완료하며 CPU80% 정도 쓴다. 샘플을 40%로 줄이니 20만개 정도로 늘어나는데 CPU 점유율이 60%밖에 안된다. 스레드가 빨라진만큼 배치를 더 키우거나 스레드 개수를 더 늘려도 될 거 같다. 소프트웨어 튜닝만으로 노트북 속도를 두 배 이상 업그레이드한 것과 같은 셈이니 보람있는 일이다.

21515- 영문빈도 조사

스레드 작업과 최적화는 웬만큼 된 것 같고 다음은 채점을 다시 해야 한다. 이 결과를 보면 1등인 배열의 손가락 분담률이 엉망이다. 채점 기준이 아직 정밀하지 않고 빈도 조사도 부정확하다. 알파벳 빈도부터 다시 조사하고 명확한 채점 기준을 마련해야겠다.

좀 더 객관적인 빈도 조사를 위해 영문 소설을 사용하기로 했다. https://www.gutenberg.org/browse/scores/top 사이트는 이북을 공유하는데 이중 인기가 많은 100개중 상위 몇 개를 골라 1M 정도의 텍스트 샘플을 만들었다. 단순 텍스트이지만 따옴표가 “로 되어 있어 "로 바꾸었다. 이전 샘플, 긴샘플, 소솔 샘플로 각각 빈도를 다시 조사했다.

 

총 타수 = 10506

Shift = 302

 

Space      1628     15.50%

e             951       9.05%

t             799       7.61%

o             599       5.70%

a             598       5.69%

r             588       5.60%

i              540       5.14%

n             512       4.87%

s             426       4.05%

h             418       3.98%

d             362       3.45%

l              326       3.10%

c             317       3.02%

u             213       2.03%

p             212       2.02%

Enter       205       1.95%

g             184       1.75%

m            174       1.66%

y             166       1.58%

f             163       1.55%

w            150       1.43%

b             128       1.22%

,              111       1.06%

.              103       0.98%

v             84        0.80%

=             80        0.76%

k             47        0.45%

;              39        0.37%

x             32        0.30%

?             24        0.23%

j              14        0.13%

q             4          0.04%

:              3          0.03%

z             2          0.02%

_             1          0.01%

!              1          0.01%

총 타수 = 1749998

Shift = 66262

 

Space      283436  16.20%

e             159860  9.13%

t             121573  6.95%

o             101625  5.81%

a             97528    5.57%

i              94133    5.38%

n             87428    5.00%

s             84193    4.81%

r             83095    4.75%

h             61045    3.49%

.              57331    3.28%

l              55433    3.17%

d             50410    2.88%

c             44693    2.55%

u             38232    2.18%

m            32460    1.85%

p             29000    1.66%

f             27527    1.57%

g             26992    1.54%

y             26907    1.54%

Enter       25059    1.43%

w            23290    1.33%

b             20703    1.18%

,              15841    0.91%

v             12134    0.69%

k             9569     0.55%

x             3713     0.21%

j              1955     0.11%

?             1848     0.11%

q             1652     0.09%

;              1350     0.08%

:              1140     0.07%

z             926       0.05%

=             644       0.04%

!              576       0.03%

_             435       0.02%

총 타수 = 1016917

Shift = 32483

 

Space      171549  16.87%

e             94984    9.34%

t             67701    6.66%

a             61891    6.09%

o             57398    5.64%

n             53567    5.27%

i              53458    5.26%

s             47472    4.67%

h             46256    4.55%

r             43719    4.30%

d             33181    3.26%

l              31829    3.13%

u             22114    2.17%

Enter       21976    2.16%

m            19696    1.94%

c             18540    1.82%

w            17728    1.74%

y             17134    1.68%

g             16432    1.62%

f             15855    1.56%

,              13475    1.33%

p             12519    1.23%

b             11679    1.15%

.              10495    1.03%

v             7348     0.72%

k             6521     0.64%

?             2081     0.20%

j              1325     0.13%

;              1294     0.13%

!              1250     0.12%

x             1097     0.11%

_             988       0.10%

q             950       0.09%

z             487       0.05%

:              445       0.04%

=             0          0.00%

10K - SampleEngShort

1.7M - SampleEng

1M - SampleEngNovel

 

비슷한 것 같으면서도 참 다르게 나온다. 알파벳 순위별로 나열해 보자.

 

10K:             ETOARINSHDLCUPGMYFWBVKXJQZ

1.7M:            ETOAINSRHLDCUMPFGYWBVKXJQZ

1M:              ETAONISHRDLUMCWYGFPBVKJXQZ

 

앞부분 e, t는 거의 고정이고 뒷부분 bvkxjqz도 모두 같다. AO의 순서가 좀 애매한데 다른 통계에는 A가 다 앞쪽이라 이 부분은 짧은 샘플의 문제였던 것 같다. 이후의 중간 순위도 약간씩 가변적이다. 그럼 다른 통계는 어떤가 같이 조사해 보자. 모스 부호 창시자나 사전 제작 업체 등에서 낸 통계는 또 다르다.

 

이전웹조사:              ETAONSHRDLICUMFGWYPBVKJXQZ

모스부호 :               ETAINOSHRDLUCMFWYGPBVKQJXZ

옥스포드 사전 :       EARIOTNSLCUDPMHGBFYWKVXZJQ

영소설 33:          ETAONISRHDLCUMFWYGPBVKXQZJ

 

이전웹조사 빈도는 지금까지 개발에 사용했던 빈도이다. 이 기준으로 최소 부담도 레이아웃을 만들었으며 채점 순회의 시작점이기도 하다. 실제 조사한 것에 비해 I의 순위가 과소평가되어 있고 F는 약간 과대평가되어 있다. 나머지는 한두칸 범위내에서 비슷하다. 이 빈도가 부정확하다 보니 채점의 정확도가 떨어진다.

모스 부호의 빈도도 직접 조사한 것과 비슷하다. 이거 만드는 사람도 빈도 높은 알파벳에 치기 쉬운 부호를 할당해야 하니 중요했을 것이다. 옥스포드 사전의 빈도는 개판이다. T가 너무 뒤쪽으로 가 있고 R은 또 너무 앞쪽이며 H도 뒤쪽이다. 영소설 33권의 빈도는 직접 조사한 것과 유사한데 이게 맞는 것 같다.

통계마다 빈도가 다른데 샘플에 대한 차이도 있지만 통계 방식에 대한 차이도 크다. 대소문자를 따로 통계를 낸다거나 문자 위치에 따라 통계를 내면 확 달라진다. 샘플에 따른 빈도차도 상당한데 JQuery 책에는 JQ의 빈도가 확 올라갈 것이다. 정확하게 앞뒤를 가리기 애매한 경우도 있는데 비슷한 순위를 묶으면 대략 다음과 같은 결과가 나온다.

 

E

T

A

O

N

I

S

H

R

D

L

U

M

C

W

Y

G

F

P

B

V

K

J

X

Q

Z

 

절대적 통계란 없으므로 공신력 걱정할 필요 없이 직접 만든 샘플의 빈도를 따르는 것이 가장 합리적일 것 같다. 자판 배열을 찾기 위한 빈도이면 충분하며 1M 소설의 빈도를 채택하기로 한다. 이제 진짜 통계에 쓸 샘플을 만들 차례이다. 요건은 다음과 같다.

 

- 50K 안팎. 연타는 일부 앞쪽만 사용할 예정

- 영문을 대표하는 문장 사용

- 중간 중간에 코드도 삽입할 것

 

너무 잡스러운 샘플을 사용하면 차후 공신력에 문제가 생길 수도 있고 한쪽으로 치우쳐도 안된다. 그래서 골고루 일부씩 섞어 다음과 같이 샘플을 채취했다.

 

*** lincoln gettysburg address

*** Bible Genesis

*** Harry Potter and the Sorcerer's Stone

*** ApiStart.cpp

*** Alice's Adventures in Wonderland

*** The Microsoft .NET Framework

*** EnumLayout.cpp

*** War and Peace

*** newmember.php - except HANGUL

*** O'Reilly Open Source and Free Software Licensing

*** Pride and Prejudice

*** GNU GENERAL PUBLIC LICENSE

 

최초 50K 길이로 만들었는데 속도에 좀 부정적이어서 32K로 줄였다. 연타 비율만 조금 줄이면 이 정도 길이는 충분히 커버할 수 있다. 32K 샘플의 문자 출현 빈도는 1M 샘플과는 조금 다르다.

 

이전빈도:      ETAONSHRDLICUMFGWYPBVKJXQZ

1M(빈도):      ETAONISHRDLUMCWYGFPBVKJXQZ

32K(샘플):     ETAONIRSHDLCUWGMPFYBVKXJZQ

 

앞부분은 거의 같은데 RSH 내에서 R이 좀 높아지고 UMC 그룹에서 C가 좀 높으며 M이 다음 그룹으로 조금 밀려 나간다. 뒷부분 JXQZ는 각각 자리를 바꿔 XJQZ인데 출현 빈도가 다 30회 수준이라 큰 의미 없으며 어차피 순회 대상도 아니다. 빈도는 1M 기준으로 하고 샘플은 32K를 쓰되 연타 비율 알고리즘을 조금 더 수정하기로 한다.

빈도와 샘플을 조정한 결과를 소스에 적용했다. 기준과 대상이 다 바뀌었으니 채점 결과도 이제 다르게 나타난다. 앞으로 계속 이 기준으로 채점하며 논리적인 이상만 수정하기로 한다. 현재 연타율이 샘플 비율에 따라 좀 다르게 나타나는 경향이 있는데 특히 20%일 때가 좀 이상하다.

알파벳이 무작위로 나타나므로 샘플의 크기와 연타율은 별 상관이 없어야 정상인데 샘플을 줄이면 연타율도 같이 줄어드는 현상이 발생한다. 왜 그럴까 싶어 생각해 보니 크게 두 가지 정도의 문제가 있는 것 같다.

 

- 기호가 영향을 미친다. if a+b==c 이런 문자열에서 +의 손가락이 a와 다를 확률이 높아 ab만 있는 경우와 달라진다. , 기호가 많이 나오는 소스 코드는 연타율이 일반 문장보다 덜하다.

-공백과 개행도 영향을 미친다. 공백은 엄지여서 앞뒤 문자와 연타일 수 없다. 공백이 많을수록 연타율이 떨어지며 개행도 마찬가지이다.

 

이런 문제를 해결하기 위해 연타율에 대한 샘플은 알파벳만 대상으로 선정하여 측정했다. 이전보다 연타율은 더 올라가겠지만 어차피 상대적인 비교여서 기준만 확실하면 별 상관이 없을 것 같다.

연타샘플비율을 짝수로만 옵션을 만들었는데 이왕 만드는 김에 슬라이드 컨트롤로 입력받도록 하여 10% 단위로 선택하도록 했다. 옵션은 가급적 상세하게 지정할 수 있어야 한다. 빈도순 배열에 대해 10%부터 차례대로 비율을 늘려가며 손가락 연타율을 조사해 보았다.

 

15.7 15.1 15.3 15.2 15.0 14.7 14.6 14.6 14.6 14.6

 

샘플 크기가 늘어날수록 연타율이 내려간다. 다른 논리적인 문제점이 있나 싶어 비율에 따라 샘플을 잘 추출하는지 메시지 박스로 조사해 봤다. 10, 20, 40, 80을 조사했는데 비율대로 잘 추출한다.

 

문자 분포가 정규적이라면 샘플 길이와 상관없이 일정한 연타율이 나와야 하는데 그렇지 않아서 좀 의아하다. 어쨌거나 상대적인 평가이니 필요한 속도에 맞게 연타율을 선택하여 일관되게 적용하면 될 거 같기는 하다. 디폴트를 40 정도로 두고 필요에 따라 축소해서 쓰기로 한다.

연타샘플비율은 전체 체점에서 일관되어야 한다. 실행중 변경은 안되지만 중간 결과를 저장해 놓고 다시 읽어올 때도 원래대로 설정할 필요가 있다. Save에서 이 비율을 저장하고 Load에서 다시 읽도록 했다. Dist와 연타비율은 한번 정하면 채점 내도록 변경할 수 없다.

연타샘플비율을 트랙바로 바꾼김에 스레드도 트랙바로 바꾸어 1~T까지 자유롭게 선택하도록 했다. 연타 샘플 비율은 전처리에서만 사용하므로 실행중에 바꾸어 봐야 아무 효과가 없지만 스레드 개수는 자유롭게 바꿀 수 있다.

속도 체크 기능을 급하다고 LBUTTONDOWN에 넣어 놨는데 비직관적이고 디버깅할 때 불편하다. 그래서 별도의 SpeedTest 버튼을 만들고 이 버튼에 기능을 작성했다. 다음에 봐도 직관적으로 쓸 수 있어야 한다.

리셋 기능도 넣을려고 시도해 봤는데 이내 관 두었다. 스레드나 자료 구조를 시작하기 전의 상태로 깔끔하게 돌리는게 쉽지 않을 뿐더러 스레드가 종료 플래그를 일일이 점검하면 오히려 더 느려지기 때문이다.

-----------------------------------------

스레드 구조를 바꾼 후 여러 가지 최적화를 통해 초당 4600번의 채점 속도가 샘플 비율 10% 기준 76000번으로 15배 정도 향상되었다. 그러나 빈도를 조정하고 샘플을 변경하는 과정까지 거치고 나니 예상 외로 그 속도가 나오지 않는다.

디버그 기준으로 초당 11만개를 채점하는 건 좋은데 CPU 사용율이 100%가 아니고 빈둥 빈둥 놀고 있다. 릴리즈는 2배 정도 속도가 더 나오지만 최선의 결과가 아니다. 싱글 스피드가 7만이면 스레드 10개를 돌리면 70만이 나와야 한다. 왜 이렇게 되는지 2개의 스레드만 돌리며 로그를 넣어 스레드 대기 시간을 측정해 봤다.

 

0 스레드 작업 전달

0 스레드 신호 대기 120776

1 스레드 작업 전달                 -- 여기서 전달했는데

0 스레드 대기 4738

0 스레드 작업 전달                 -- 다음 스레드 순회분 만들때까지도

0 스레드 신호 대기 124

1 스레드 신호 대기 110974      -- 1번 스레드가 작업에 들어가지 않음. 이벤트 신호 전달이 느린 모양임.

1 스레드 대기 4044                  -- 1번 스레드가 늦어져 메인 대기함.

1 스레드 작업 전달

 

0번 스레드에 작업을 전달하면 0번이 동작을 시작한다. 초기에 12만번을 기다리는 건 그럴 수 있다. 그동안 메인은 1번에게 작업을 전달하고 0번이 끝날 때까지 4700번 기다린 후 다시 작업을 전달하며 0번이 대기 거의 없이 작업을 바로 시작한다. 그러나 1번은 이때서야 작업을 시작하는데 0번이 한타임 더 뛰는 동안에도 놀고 있었다는 얘기다. 이게 늦으니 메인은 또 1번을 기다리고 있는 셈이다.

main이 작업거리를 늦게 줘서 그런가 싶어 Sleep을 제거해 봐도 효과가 없다. 한 스레드가 처리하는 배치값이 낮아서 그런가 싶어 BATCUNUM200이나 1000으로 늘려도 약간 빨라질 뿐 별 변화가 없다. 배치는 적당히 커야 하지만 대기 시간의 비율만 줄여주면 될 뿐이라 100 이상은 큰 의미가 없다.

현재 main의 작업 배분 기준은 tid를 스레드 수만큼 순회하며 작업을 던져 주고 다음 스레드로 이동하여 대기 후 또 작업을 던져 주는 식이다. 0번 지시, 1번 대기, 1번 지시, 2번 대기, 2번 지시 이런 순인데 이런 순차적 흐름이 문제인 것 같다. main은 스레드에게 작업을 던져 주면 다음 사이클이 올 때까지 다 끝내고 대기하고 있을 것으로 가정하는데 이 가정이 맞지 않다.

 

do {

        tidx에게 작업 하나 전달

        if (배치꽉참) {

                    tidx 작업 시작 지시

                    다음 tidx로 이동

                    다음 tidx가 작업 끝냈는지 대기

        }

} while NextLayout();

 

스레드가 CPU를 받는 시간은 엿장수 마음대로라 한바퀴 돌고 와도 이 스레드가 실행을 다 마치지 못했을 확률이 낮지 않다. 그러다 보니 다른 스레드까지도 전부 대기하게 되고 CPU를 제대로 활용하지 못하는 것이다. main에서 순차적으로 작업을 지시하지 말고 먼저 끝낸 놈을 찾으면 대기 시간이 줄어든다. main의 구조를 완전히 다시 작성해야 한다. 다음과 같이 수정했다.

 

for(;;) {

        비신호 상태의 스레드 찾음. 못 찾으면 대기

        BATCHNUM개의 작업 거리 전달

                    적합 배열만

                    샘플링 대상만

                    NextLayout - 끝 처리

        작업 지시 전달

        끝이면 종료

        Exit, Pause 처리

}

 

이렇게 바꾼 후 테스트해 보니 디버그는 초당 16만개, 릴리즈는 초당 30만개씩 처리하며 CPU 점유율은 80 ~ 100 사이를 왔다 갔다 한다. 스레드끼리 대기하는 시간이 좀 있다 보니 가끔씩 쉬어가는 시간이 있다.

사용 스레드 개수를 줄이면 점유율은 떨어고 모든 스레드를 다 사용하면 거의 100%에 가까운 성능을 내기도 한다. 굳이 그럴 필요 없이 T-3 정도면 괜찮은 것 같고 5600만개를 34초에 채점 완료하니 이 정도면 충분하다. 릴리즈에 연타샘플은 10%로 하고 성능 측정 결과는 다음과 같다.

 

데탑 : 12스레드. 34/. 245. 80~100% 4.28GHz

놋북 : 13스레드. 41/. 216. 80% 4.0GHz

 

놋북에 Enum 돌리고 있어 스레드를 다 안 썼음에도 더 성능이 좋다. 빠른 컴퓨터에서 빠른 성능이 제대로 나오고 있다. 최초 예측은 12시간이었다가 33분으로 줄었다가 지금은 2분 안으로 들어왔으니 대단하다. 대기 시간을 더 줄여 극한의 성능을 낼 수도 있지만 성능 튜닝은 여기서 그만 두고자 한다.

이거 돌려서 나오는건 고작 최적 배열 하나인데 그거 얻자고 너무 많은 시간을 투자하는 것 같다. 사실, 레이아웃 그 자체보다는 합리적으로 계산했다는 근거를 만드는데 너무 치중하고 있는 것 같다. 소스 리팩토링하고 배점 정도만 조정한 후 마무리하고자 한다. 이거 말고도 회사일이나 개인적인 공부나 할게 많은데 너무 많은 시간을 뺏기고 있다는 느낌이 든다.

-----------------------------------------

다음은 점수 변별력을 조정할 차례이다. 뭔가 채점을 하기는 하는데 과연 제대로 하고 있는지, 좋은 레이아웃을 잘 선정하는지 점검 및 조정이 필요하다. 다음은 dist2로 조사한 Best100 결과이다.

 

     5279,9178    atenoshirdlucymwpbgfvkjxqz 83.68점 부 15.53 53:47 14:20:29:35-45:23:26:04 26:57:16 지연 9% 손연 36% 연철 0

     5279,9478    atenoshirdluwmycpbgfvkjxqz 82.64점 부 15.54 53:47 14:19:29:35-45:22:27:04 25:57:16 지연 9% 손연 36% 연철 0

     5279,9778    atenoshirdluwymcpbgfvkjxqz 82.64점 부 15.54 53:47 14:20:29:35-45:22:27:04 26:57:16 지연 9% 손연 36% 연철 0

     5280,8896    atenoshirdmulycwpbgfvkjxqz 82.63점 부 15.54 54:46 14:21:28:35-43:25:26:04 27:57:14 지연 9% 손연 37% 연철 0

     5280,1355    atenoshirdlmwuycpbgfvkjxqz 82.60점 부 15.54 53:47 14:19:29:35-45:22:27:04 25:57:17 지연 9% 손연 36% 연철 0

     5279,9172    atenoshirdlucymwpfgbvkjxqz 82.47점 부 15.55 53:47 13:20:29:36-45:23:26:04 25:58:16 지연 9% 손연 36% 연철 0

     5279,9177    atenoshirdlucymwpfgkvbjxqz 82.41점 부 15.56 53:47 13:20:29:36-45:23:26:04 24:58:17 지연 9% 손연 36% 연철 0

     5280,8895    atenoshirdmulycwpfgkvbjxqz 82.37점 부 15.56 54:46 13:21:28:36-43:25:26:04 26:58:15 지연 9% 손연 37% 연철 0

      690,9884    etonashiruldwcmgpyvfbkjxqz 82.24점 부 15.48 55:45 14:20:25:39-44:23:26:05 27:56:15 지연 8% 손연 37% 연철 0

      690,9872    etonashiruldwcmgfyvpbkjxqz 82.24점 부 15.48 55:45 14:20:25:39-44:23:26:05 27:56:15 지연 8% 손연 37% 연철 0

....

      248,0615    etanoshilurdwmycpfgbvkjxqz 80.56점 부 15.54 53:47 13:19:25:41-45:22:27:04 24:58:17 지연 8% 손연 36% 연철 0

      694,7799    etonashilurdwymcfpgbvkjxqz 80.53점 부 15.55 53:47 13:21:23:41-45:22:27:04 24:58:17 지연 8% 손연 36% 연철 0

     5264,7999    atenosihrdlucmwgybvfpkjxqz 80.48점 부 15.45 54:46 16:20:29:33-43:23:25:06 26:57:16 지연 9% 손연 38% 연철 0

 

채점에 논리적인 문제가 없는지 디버그, 릴리즈 각각 돌려 차이를 비교해 봤다. 다 똑같은데 두 레이아웃의 순위가 바뀌어 있어 순간 깜짝 놀랐다. 동점인 경우인데 스레드 실행 순서에 따라 달라질 수도 있으니 문제가 아니다. 똑같은 결과인데도 순서가 달라질 수 있는 미세한 차이가 있어 역시 멀티 스레드는 난해하다.

최고점이 83.68이고 100등이 80.48점으로 순위간 차이가 나기는 한다. 이 차이가 과연 합리적인지 점검해 보자. 먼저 기준이 되는 빈도순을 보자. 시작점이기 때문에 이 레이아웃의 점수를 참고하여 채점해야 한다.

 

빈도순 : etaonishrdlumcwygfpbvkjxqz

부담도 15.47. -2.29

좌우 54.3:45.6 0

14:20:23:42-46:23:25 21

23:59:18 4

지연타 : 15%. 30

손연 : 51%. 30

연철 감점 : 14

96.71감점. 총점 3.29

 

부담도는 단연 제일 낮으며 오히려 득점을 한다. 좌우 비율은 왼손이 오른손보다는 한단계 더 높은 우선순위를 가지도록 되어 있어 이대로 해도 적당하다. 손가락 분담률은 감점이 좀 많은데 빈도순이어서 집게에 부담이 많이 간다. jxqz가 빈도는 낮지만 fp자리의 부담도가 더해져 크게 나온다. 집게 자리를 중지, 약지로 좀 분산시켜야 한다.

행은 가운데가 너무 몰려 있어 감점이다. 1행에 좀 더 분산시키는 것이 좋되 2행을 더 적극적으로 사용해도 될 것 같다. 현재 만점 기준은 27:55:18이며 약간 더 분산시키는 것으로 되어 있다. 지연 손연은 적을수록 좋은데 단순 빈도순은 이를 고려하지 않아 감점이 심하다. 연철도 마찬가지이다.

빈도순을 기준점으로 참고하여 최고점을 받은 레이아웃과 100위의 성적표를 비교해 보았다. 그래픽 출력 기능을 만들어 놨더니 일일이 그려 보지 않아도 레이아웃을 한눈에 볼 수 있어 참 편리하다.

 

1: atenoshirdlucymwpbgfvkjxqz

부담도 15.53. -1.68

좌우 53:47. 0

14:20:29:35-45:23:26:04. 15

26:57:16. 0

지연타 : 9%. 12

손연타 : 36%. -9

연철 감점 : 0

16.32감점. 총점 83.68

 

100: atenosihrdlucmwgybvfpkjxqz

부담도 15.45. -2.48

좌우 54:46. 0

16:20:29:33-43:23:25:06. 13

26:57:16. 0

지연타 : 9%. 12

손연타 : 38%. -3

연철 감점 : 0

19.52감점. 총점 80.48

 

1등의 부담도가 미세하게 약간 더 좋다. 좌우는 둘 다 균등하고 행분담률도 둘 다 만점이다. 손가락 분담률이 둘 다 좋지 않다. 30:28:28:12를 만점으로 하는데 이 비율을 좀 더 조정해야 한다. 왼손은 그나마 괜찮은데 오른손은 집게에만 부담이 너무 높게 나타난다. 좌우손의 손가락 분담률도 굳이 꼭 같을 필요는 없다.

손가락 연타율은 5%를 만점으로 잡았는데 지금은 알파벳만 모아 측정하는 방식이라 만점을 더 높여야 한다. 손연타율은 40%인데 이 또한 더 높게 조정하고 허용치를 넓게 잡아 주어야겠다. 연철은 보이는대로 감점하는게 옳되 다른 감점과 밸런스 조정을 조금 더 하면 될 것 같다. 현재의 기준과 각 레이아웃별 점수를 도표로 정리했다.

 

기준

만점

허용

가중치

빈도순

1

100

조정

부담도

15.7

 

10

15.47

15.53

15.45

15.4 만점 가중치 100. 0~40 변별

좌우

53

+-2

제곱

54.3

53

54

52 만점. +-3허용. 초과분 제곱 감점. 0~60 변별

30:28:28:12

+-2

0.5

14:20:23:42

14:20:29:35

16:20:29:33

14:23:26:37-42:24:24:10 +-3허용. 초과분 제곱 감점.

27:55:18

+-2

1

23:59:18

26:57:16

26:57:16

25:57:18 만점 +-4 허용. 초과분 제곱 감점

지연타

5

 

3

15

9

9

8%만점 3배수. 0~45 변별

손연타

40

 

3

51

36

38

36%만점 2배수. 0~50 변별

연철

0

 

1

14

0

0

현재 기준대로 연철별로 개별 감점. 0~58 변별

점수

100

 

 

3.29

83.68

80.48

 

 

현재 상황을 참고하여 새로 마련한 조정 정책을 간략하게 정리해 놨다. 채점에는 여러 사항을 고려해야 하며 왜 그렇게 채점했는지 명확한 근거를 남겨야 한다.

채점은 점수간의 연관성이 있는 고차 방정식이다. 부담도가 좋으면 골고루 잘 분산되어 있다는 얘기이며 좌우, 손가락, 행 분담률도 같이 좋아진다. 그러나 부담도가 좋아도 분담률이 엉망일 수는 있다. 부담도를 기준으로 변별하고 적당히 허용도를 지정하여 어느 범위 내는 감점하지 않되 과락 수준으로 엉망이면 대량의 감점을 주면 된다. 연타, 연철은 부담도와는 별 상관이 없는 독립 변수며 낮을수록 좋다.

점수를 매기는 방식은 두 가지가 있다. 현재는 100점 만점에서 감점하는 방식을 사용하는데 득점 방식으로 바꾸는 것도 고려해 보았다.

 

- 감점(negative) :  이상적 수치를 제시한 후 여기서 멀어지면 감점을 주어 엉망인 레이아웃을 걸러낸다. 이상값을 설정하기 쉽고 객관적이지만 낮은 감점이 더 좋은 배열이어서 비직관적이다. 100에서 빼서 반대로 표현하지만 감점이 높으면 음수도 가능해 역시 어색하다. 아예 감점 자체를 점수로 취급하는 것이 더 분명하긴 하다.

- 득점(positive) : 0점에서 시작해서 장점에 대해 득점을 부여하는 방식이다. 0점을 어디로 규정하는지가 어려운데 가장 개판인 레이아웃을 찾기도 쉽지 않다. 또 여기서 좋아진 정도를 계산하여 득점 정도를 조정하는 것도 어렵다. 만점이 100점으로 나오지도 않는다.

 

두 방식중 단순 감점 방식을 채택하기로 한다. 이상적인 배열에서 가장 덜 멀어지는 배열을 찾는 방식이다. 100점을 고집할 필요도 없고 감점 총합을 그냥 점수로 취급하면 된다.

어떤 방식이든 레이아웃의 우열을 가리는 변별력이 있어야 한다. 이를 위해 나름 엉망인 배열의 점수를 참고해야 한다. 이런 목적으로 쿼티와 알파벳순 배열을 채점하는 기능을 만들어 두었다. 이 점수를 거의 바닥이라고 보면 된다.

 

쿼티 : fjdkslaruvmeicwoxghqpztybn

부담도 20.35. -46

좌우 55:44. 0

13:15:33:38-41:19:32:7 21

49:32:18. 41

지연타 : 11%. 18

손연타 : 50%. 30

연철 감점 : 1

157감점. 총점 -57

 

알파벳 : nqmrlskdgwzchvbiuopajtefxy

부담도 20.06. -43

좌우 61:38. 46

27:12:9:50-21:34:42:3. 45

44:35:21. 33

지연타 : 17%. 36

손연타 : 50%. 30

연철 감점 : 3

236감점. 총점 -136

 

여러 가지 배열의 데이터를 바탕으로 하여 채점 기준을 다시 조정했다.

 

- 부담도 : 이 부분에서 엄청난 실수가 발견되었다. 최초 만점을 빈도순인 15.47 정도에서 가장 엉망인 쿼티의 20.35까지 약 5점 차가 있으니 가중치 10을 적용하면 최대 50점만큼 변별력이 있다고 생각했다. 그러나 빈도순에서 순회한 배열은 변형이 많지 않아 부담도가 20까지 가지 않는다. 과연 얼마나 차이가 있는지 코드를 작성하여 최저, 최고 빈도를 조사해 봤더니 15.4013~15.6304까지이며 차이는 고작 0.2291밖에 되지 않는다.

dist3으로 하면 좀 더 벌어지기는 하겠지만 범위가 극히 좁다. 가중치 10을 적용해 봐야 2.2점밖에 차이가 나지 않아 변별력이 거의 없는 셈이다. 만점을 15.40으로 잡고 15.80까지는 있다 보면 범위는 0.4가 된다. 여기에 가중치 100을 곱하면 최대 40점까지 변별력을 가진다. 쿼티는 500점 정도 감점되어 버리는 셈이라 과락 처리될 것이다.

 

- 좌우 분담률 : 53:47 만점에 +-2까지 허용하고 가중치는 초과의 제곱으로 처리했었다. 왼손에 더 높은 비중을 둔 것은 편집과 마우스를 고려한 주관적인 기준이었는데 여전히 유효하다. 이 값도 dist2 기준으로 범위가 어디까지 걸치는지 조사할 필요가 있다. 코드 작성해서 최소, 최대값을 조사해 보니 43~63 범위이며 최대 20만큼이나 벌어진다.

좌우 분담률은 허용 범위가 다소 넓은 편이다. +-3까지는 무감점 처리하되 그러면 왼손이 56~50이 되니 좀 많아 보인다. 만점을 52:48로 조정하여 왼손 분담률 55~49까지는 봐주고 초과분에 대해 제곱으로 감점한다. 43이면 -36, 63이면 -64점 감점되어 60점 가량의 변별력을 가지는 셈이다.

 

- 손가락 분담률 : 35:30:20:15 만점에 +-2 허용, 초과분 감점은 너무 대충 정한 기준인 것 같다. 일단 집게가 힘이 좋고 더 많은 키를 배정받았으니 35보다는 더 높은 비중을 가져야 한다. 또한 좌우 손의 역할이 조금 달라 꼭 같은 비율일 필요는 없다. 여러 배열의 현재 비율을 참고하여 만점 비율을 새로 정했다.

 

배열

분담율

+-2, 초과분 감점

+-3, 제곱 감점

빈도

14:20:23:42-46:23;25:21

1,1,3,2,9=16

4,1,64=69

1

14:20:29:35-45:23:26:04

1,1,1,4=7

9

100

16:20:29:33-43:23:25:06

1,1,1,2=5

1

쿼티

13:15:33:38-41:19:32:07

6,5,3,6,1=21

25,16,4,16=61

만점

14:23:26:37-42:24:24:10

 

 

 

만점 비율을 설정한데는 비록 주관적이지만 다 근거가 있다. 오른손 새끼에 10밖에 안준 이유는 새밖열에 기호가 존재하기 때문이고 오른손 집게에 부담을 많이 준 이유는 힘이 제일 좋기 때문이다. 나머지 수치는 기존 레이아웃의 분담도를 참고했다.

만점에서 점수를 매기는 방식은 두 가지를 생각했는데 +-3만큼 허용하고 제곱으로 감점하는 방식을 채택하기로 한다. 손 단위로 백분율이라 +-3만큼은 허용해도 될 거 같고 이상적인 배열에서 멀어질수록 확실히 쳐 내기 위해 제곱으로 감점했다.

 

- 행 분담율 : 27:55:18 기준에 +-2 허용, 초과분만큼 감점으로 되어 있다. 25:57:18로 만점 약간 조정하되 +-4만큼 허용하고 초과 제곱분만큼 감점하기로 한다. 허용도가 높은만큼 범위 안에만 들면 웬만해서는 감점이 약하지만 쿼티처럼 과하게 벗어나면 가차없이 내치는 작전이다.

 

- 손가락 연타율 : 5% 만점에 3배수를 적용했으며 이보다 적으면 마이너스 감점도 가능하다. 부담도와는 상관이 없으며 낮을수록 좋아서 허용치는 적용하지 않는다. 레이아웃의 품질을 결정하는 중요한 기준이다. 5%는 예전 통계 방식을 기준으로 한 것이며 지금은 알파벳만 추려 연타율을 집계하므로 이보다는 더 상향 조정해야 한다. 레이아웃별로 15, 9, 9, 11, 17 정도의 연타율을 보인다.

정확한 분포 범위가 필요해 또 코드를 돌려 보았는데 최소 8.09% ~ 23.25% 범위에 걸치며 대략 15%차가 난다. 만점은 8%로 잡고 3배수를 적용하면 최대 감점은 45점이다.

 

- 손 연타율 : 손가락 연타율과 비슷한 방법으로 지정한다. 기존 레이아웃은 36~51의 범위에 있으며 전체 레이아웃을 조사해 보니 36.79 ~ 63.73 범위에 27%차가 난다. 만점을 36%로 잡고 가중치는 2만 적용하여 최대 50점 정도 감점된다.

 

- 연철 감점은 현재 기준으로 한다. 13점짜리 3개 있으면 거의 과락에 해당하는데 일부러 과락하는 코드는 삭제하기로 한다. 어차피 39점 감점 맞으면 BEST에는 들지 못하고 빈도순 순회에는 이런 예가 흔하지 않다. 총 감점은 58점인데 일부러 만들지 않는 한 이런 배열은 나올 수 없다.

 

여기까지 작업한 후 새로 만든 기준을 적용했다. 점수를 감점으로 바꾸다 보니 정렬이나 비교 함수를 전부 바꾸어야 해서 잔손질을 좀 많이 했고 함정이 많아 시간이 좀 걸렸다. 점수라는 용어부터 좀 비직관적인데 차후 감점으로 바꿔야겠다. 적용한 후의 채점 결과는 다음과 같다. 다행히 또는 당연히 기준이 바뀌었다고 해서 채점 속도가 느려지지는 않았다.

여기까지 수정한 후의 채점 결과는 다음과 같다. 결과가 과연 똑바른지 점검해 볼 필요가 있다.

 

      690,9960    etonashiruldwymcgbvfpkjxqz 10.89점 부 15.47 54:46 14:21:23:40-43:22:27:06 28:56:14 지연 8% 손연 38% 연철 0

      244,2680    etanoshiruldwmcgpyvfbkjxqz 11.38점 부 15.47 55:45 14:21:24:39-44:23:26:05 28:56:15 지연 8% 손연 37% 연철 0

      690,9884    etonashiruldwcmgpyvfbkjxqz 11.60점 부 15.48 55:45 14:20:25:39-44:23:26:05 27:56:15 지연 8% 손연 37% 연철 0

      690,9874    etonashiruldwcmgfyvbpkjxqz 11.62점 부 15.48 54:46 13:20:25:40-43:22:26:06 27:56:15 지연 8% 손연 38% 연철 0

      690,9872    etonashiruldwcmgfyvpbkjxqz 11.62점 부 15.48 55:45 14:20:25:39-44:23:26:05 27:56:15 지연 8% 손연 37% 연철 0

 

1등을 한 배열의 점수가 10.89점인데 손가락 연타가 300회 있고 8.93%이지만 감점이 없는 것으로 되어 있다. 0.93이나 초과했는데 감점 필드가 정수이다 보니 버려지고 정확하게 계산되지 않았다. 변별력을 높이려면 모든 감점 필드를 다 실수로 선언하고 소수점 이하까지 계산해야 한다. 정수로 바꾸기 위해 반올림하는 처리도 모두 제거하고 출력문의 %d%f로 교체했다. 수정 후 다시 채점한 결과는 다음과 같다.

 

      690,9960    etonashiruldwymcgbvfpkjxqz 14.18점 부 15.47 54:46 14:21:23:40-43:22:27:06 28:56:14 지연 8% 손연 38% 연철 0

      690,9360    etonashiruldcymwgbvfpkjxqz 14.23점 부 15.47 54:46 14:21:23:40-43:23:26:06 28:56:14 지연 9% 손연 38% 연철 0

      690,9874    etonashiruldwcmgfyvbpkjxqz 14.38점 부 15.48 54:46 13:20:25:40-43:22:26:06 27:56:15 지연 8% 손연 38% 연철 0

      690,9746    etonashiruldwmgcfyvbpkjxqz 14.66점 부 15.48 54:46 13:21:24:40-43:22:27:06 28:56:15 지연 8% 손연 38% 연철 0

      690,9101    etonashiruldcmgwfyvbpkjxqz 14.70점 부 15.47 54:46 13:21:24:40-43:23:26:06 28:56:15 지연 8% 손연 38% 연철 0

 

1등은 여전히 같은데 실수 단위로 감점하니 점수가 14.18로 더 높아졌다. 분담률 감점은 0.05인데 초과분이 0.1 정도일 때 제곱하면 오히려 더 작아지기도 한다. 1차배치 레이아웃은 감점이 83점으로 꽤 높게 나타난다. 손가락 분담률에서 32점을 까먹고 행분담률에서 13, 손연타에서 9점을 까 먹었다. 새로 만든 기준에 대해서는 확실히 좀 떨어지는 배열이기는 하다. 1차배치 자체는 아예 순회 대상에 포함되지도 않는다.

 

1차배치 :     ETONISLFDUMARGHCPKVWYBJXQZ

기준빈도 :    ETAONISHRDLUMCWYGFPBVKJXQZ

 

기준빈도와 비교해 보면 LF가 너무 앞쪽이고 A는 또 너무 뒤쪽에 있다. 여러 가지 이유로 인위적인 배치를 만들다 보니 자연스러운 빈도와는 어울리지 않았던 것 같다. dist를 충분히 늘리면 이런 배치에 대해서도 점수를 매겨볼 수 있기는 한데 시간이 너무 오래 걸릴 것 같다.

그래서 일단 dist3이나 dist4로 후보를 먼저 출인 후 베스트에 든 배열에 대해서는 좀 더 광범위하게 앞뒤로 섞어 보는 방법을 생각해 봤다. 이 처리를 하려면 후보가 많아야 하므로 BEST1000개로 늘렸다. 스레드에서 베스트를 집계하는 방식이 효율적이어서 개수를 늘린다고 해서 과히 느려지지는 않는다.

1차 처리 후 베스트를 대상으로 주변을 탐색해 보면 의외로 또 괜찮은 배열을 찾을지도 모른다. 베스트 개수가 늘어 나는 바람에 BestBuf10배 늘렸다. 또 속도가 빨라져 만개 단위 출력은 이제 별 의미가 없다. 최소 10만부터 시작해 100, 천만으로 출력 주기를 늘렸다. 997등부터 1000등까지는 다음과 같다.

 

      244,1967    etanoshiruldcmwgfybkpvjxqz 21.71점 부 15.48 53:47 12:20:25:41-43:23:25:06 27:57:15 지연 9% 손연 40% 연철 0

      244,1405    etanoshiruldmwcgyfvpbkjxqz 21.71점 부 15.49 55:45 14:20:24:40-44:23:26:05 28:56:14 지연 9% 손연 39% 연철 0

      690,9470    etonashiruldwmcygfvbpkjxqz 21.71점 부 15.49 54:46 13:21:24:40-44:23:25:07 27:56:15 지연 9% 손연 40% 연철 0

 

부담도는 거의 차이가 없고 좌우, 손가락, 행 분담률도 거의 만점이다. 손가락 연타가 4.22로 약간 많고 손연타에서 8점 감점이 발생하여 좀 많이 벌어졌다. 부담도의 차이에 비해 연타율의 차이가 너무 큰 영향을 미치는 것 같다. 점수간의 균형을 좀 더 조정해야 한다.

전체 레이아웃을 기준으로 하면 적절하지만 베스트 1000만 보면 아직도 변별력이 많이 떨어져 특정 기준을 조정할 필요가 있다. 빈도순에서 출발하고 dist가 고작 2여서 다 비슷비슷하며 그래서 차별성을 두기 어렵다.

 

- 부담도는 15.4~15.51까지에 걸친다. 겨우 0.05 차이여서 5점밖에 차이가 나지 않는다. 배수를 300으로 지정하여 전체 120, 베스트내에서도 15점까지는 차이가 나도록 한다.

- 좌우 분담률은 왠만하면 0이고 최고 감점이 0.19에 불과하다. 허용치를 1 낮추어 2로 조정한다. 4이상 벗어나야 4점 감점이다.

- 행분담률은 왠만하면 0이고 많아 봐야 0.8이어서 거의 영향을 미치지 못한다. 사실 그리 중요한 기준도 아니긴 하다. 2행의 허용치는 4를 유지하고 1, 3행의 허용치를 3으로 낮추어 조금 더 감점되도록 한다.

- 손가락 분담률은 0~6.13 정도에 분포하여 적당하다. 이대로 유지한다.

- 손가락 연타는 1.1~7.7사이여서 역시 적당하다. 이대로 유지한다.

- 손연타는 3~10.2로 좀 많다. 치명적인 기준은 아니어서 만점을 1점 더 높여 37%로 조정한다. 이론상 36%이면 마이너스 감점도 나올 수 있다.

- 가장 치명적인 문제는 연철에 대한 감점인데 하나에 13점이나 깍아 버리면 너무 가혹하다. 그래서 베스트에는 연철이 거의 나타나지 않는다. er, th 주요 두 연타에 3점 정도 감점하고 나머지는 1점이나 0.x 정도로 경미하게 감점시킨다. 이를 위해 연타 감점도 실수로 바꿔야 한다.

 

여기까지 조정한 후 dist2에 대해 상하위 순위를 다시 조사해 보자. 베스트 결과를 좀 더 보기 쉽게 감점도 같이 표시했다.

 

     5264,7999   atenosihrdlucmwgybvfpkjxqz 감점25.04 15.45(15.64) 좌우54:46(0.0) 손가락16:20:29:33-43:23:25:06(0.8) 26:57:16(0.0) 지연9%(5.0) 손연38%(3.5) 연철0.0

      229,1318   etanosihruldcmwgybvfpkjxqz 감점25.20 15.42(5.68) 좌우54:46(0.0) 손가락16:20:25:38-43:23:25:06(0.0) 27:57:14(0.0) 지연12%(14.0) 손연38%(3.5) 연철2.0

     5264,8571   atenosihrdlucymgwbvfpkjxqz 감점25.24 15.46(16.83) 좌우54:46(0.0) 손가락16:20:29:33-43:23:25:06(0.3) 26:57:16(0.0) 지연9%(4.6) 손연38%(3.5) 연철0.0

      675,8721   etonasihruldcymwgbvfpkjxqz 감점25.29 15.42(7.21) 좌우54:46(0.0) 손가락16:21:23:38-43:23:26:06(0.0) 27:57:14(0.0) 지연12%(13.6) 손연38%(2.5) 연철2.0

     5264,8699   atenosihrdluwmcgybvfpkjxqz 감점25.90 15.46(16.81) 좌우54:46(0.4) 손가락16:20:29:33-43:22:26:06(0.9) 26:57:16(0.0) 지연9%(4.8) 손연38%(3.0) 연철0.0

      244,1957   etanoshiruldcmwgybvfpkjxqz 감점25.91 15.46(17.98) 좌우54:46(0.0) 손가락14:20:25:40-43:23:25:06(0.0) 28:56:14(0.8) 지연9%(3.6) 손연38%(3.5) 연철0.0

     5264,8498   atenosihrdlucymwgbvfpkjxqz 감점25.94 15.46(16.54) 좌우54:46(0.0) 손가락16:20:29:33-43:23:26:06(0.2) 26:57:16(0.0) 지연10%(6.7) 손연38%(2.5) 연철0.0

      229,2018   etanosihruldwmcgybvfpkjxqz 감점25.98 15.42(6.84) 좌우54:46(0.4) 손가락16:20:24:37-43:22:26:06(0.0) 27:57:14(0.0) 지연12%(13.7) 손연38%(3.0) 연철2.0

      690,9360   etonashiruldcymwgbvfpkjxqz 감점26.02 15.47(19.51) 좌우54:46(0.0) 손가락14:21:23:40-43:23:26:06(0.0) 28:56:14(0.8) 지연9%(3.2) 손연38%(2.5) 연철0.0

      675,9321   etonasihruldwymcgbvfpkjxqz 감점26.02 15.43(8.37) 좌우54:46(0.0) 손가락16:21:23:38-43:22:27:06(0.0) 27:57:14(0.0) 지연12%(13.2) 손연38%(2.5) 연철2.0

     5264,8226   atenosihrdlucmgwybvfpkjxqz 감점26.13 15.45(15.64) 좌우54:46(0.0) 손가락16:20:29:33-43:23:26:06(0.9) 26:57:16(0.0) 지연10%(7.2) 손연38%(2.5) 연철0.0

....

     5278,3893   atenosidrhlmcuwgfyvbpkjxqz 감점34.99 15.47(22.12) 좌우54:46(0.0) 손가락15:20:29:34-43:23:25:06(0.6) 24:57:18(0.0) 지연10%(8.8) 손연38%(3.5) 연철0.0

      232,9641   etanosihlurdcymwgbvfpkjxqz 감점35.00 15.44(12.47) 좌우54:46(0.0) 손가락16:20:24:38-43:23:26:06(0.0) 25:57:17(0.0) 지연14%(18.1) 손연38%(2.5) 연철2.0

      244,2884   etanoshiruldwcmygbvfpkjxqz 감점35.01 15.47(20.04) 좌우54:46(1.0) 손가락14:20:25:39-44:23:25:07(0.0) 27:56:15(0.0) 지연10%(8.0) 손연40%(6.0) 연철0.0

     5265,0761   atenosihrdlmwugcfyvbpkjxqz 감점35.01 15.47(21.05) 좌우54:46(0.0) 손가락15:20:29:34-43:22:27:06(0.7) 24:57:17(0.0) 지연11%(10.8) 손연38%(2.5) 연철0.0

     5278,1686   atenosidrhlucmwgyfvkpbjxqz 감점35.01 15.49(25.89) 좌우54:46(0.0) 손가락15:20:29:34-43:23:25:06(0.6) 23:58:18(0.0) 지연9%(5.0) 손연38%(3.5) 연철0.0

     5279,4429   atenosidlhruwmgcybvfpkjxqz 감점35.01 15.48(24.96) 좌우54:46(0.0) 손가락16:20:29:33-43:22:27:06(0.9) 22:57:20(0.0) 지연10%(6.7) 손연38%(2.5) 연철0.0

 

과연 1등할 놈이 1등을 하는지 1000등은 1등에 비해 그만큼 부족한지 검증해 봐야 한다. 일단 순위가 좀 바뀌기는 했으며 변화가 생겼다. 1등이 25, 1000등이 35점 감점이니 10점 정도 차이가 난다. 분담율에는 별 차별성이 없고 부담도, 연타율에서 주로 차이가 발생한다.

1등과 1000등의 차이가 사실상 거의 없는 셈이다. 부담도는 0.03차이, 연타율도 기껏 1% 미만 차이로 등수가 확 벌어진다. 과연 이런 식으로 최적 레이아웃을 찾아내는 것이 합당한 시도인지 의구심이 들기 시작했다. 시작점이 빈도순이다 보니 여기서 크게 벗어나지 못하는 한계가 분명히 있다.

지금은 dist가 너무 작아 그럴 수도 있다. 여러 가지 시도를 해 봤고 조정도 해 봤는데 주관적이다 보니 명쾌하게 좋은 배열을 찾아내기 어렵다. 이 채점 그대로 dist3을 일단 시도해 보고 난 다음에 다시 조정을 하든가 해야겠다. 지금 속도면 dist3 133억개 채점은 13시간 정도 걸릴 것으로 예상된다.

----------------------------------

출근하기 전에 dist3으로 프로그램을 돌려 놓고 나갔다. 퇴근할 때쯤에는 다 계산해 놓을거라고 예상했는데 집에 들어와 보니 웬걸 21억까지 채점하고 다운되어 있었다. 21억이면 int 타입의 양수 상한에 걸린 것임을 딱 봐도 알 수 있다. 현상 확인을 위해 다시 돌려 봤는데 21억까지 너무 멀어 변수 조작해서 20억부터 시작하도록 했다.

그랬더니 초당 처리 개수가 계속 감소하면서 매번 결과를 재출력하고 있었다. 이유를 분석해 보니 lastPrintIdint 타입으로 선언되어 있어 21억을 넘으면 음수가 되며 따라서 매번 출력하고 있던 것이다. 레이아웃 id는 다 64비트로 선언했는데 얘만 32비트여서 문제가 발생한 것이다. __int64로 바꾸어 문제를 해결했다. 21억이라는 숫자를 보고 문제를 직감적으로 파악하는 개발자의 능력이 놀랍다.

다음은 최적화를 좀 더 했다. 연타 점검에 시간을 제일 많이 쓰는데 베스트에 아예 들지 못할 레이아웃이라면 연타를 볼 필요가 없다. 이를 위해 전역 변수 MaxMinusInBest로 최대 고감점을 기록하고 연타 보다 다른 점수를 먼저 계산한 후 중간 점수가 최대 고감점 이상이라면 굳이 연타를 점검하지 않도록 했다.

연타 감점을 뺀 점수가 이미 최대 감점을 넘었다면 연타는 더 이상 볼 필요도 없는 셈이다. 이렇게 되면 연타 점검 횟수가 줄어 들어 속도가 빨라지고 CPU 점유율은 떨어진다. 속도가 빨라졌으니 연타 샘플의 디폴트를 40% 정도로 상향 조정했다. 이 상태에서 과연 얼마나 걸러내는지 테스트해 보았다.

 

100만개 : 37만개(37% 건너뜀) -> 48만개

1000만개 : 607만개(60% 건너뜀) -> 696만개

2000만개 : 1395만개(70% 건너뜀) -> 1606만개

4000만개 : 2818만개(70% 건너뜀) -> 3118만개

 

처음에는 건너뛰는 비율이 낮다가 개수가 늘어나면 비율이 점점 높아진다. 2000만개를 넘어가면 70% 이상 건너뛰지만 대신 샘플 크기가 40%로 늘어나 속도는 그다지 많이 향상되지 않는다. 중간 점수에 미리 연타 감점을 각각 2씩 더한 값과 비교하여 좀 더 많이 걸러내도록 했다.

어차피 중간 점수가 낮은 놈이 연타율만 낮을리가 없기 때문이다. -> 다음의 숫자가 4점 감점 후의 갯수이다. 걸러내는 비율이 극적으로 증가하지는 않고 소폭 증가하였다. 대상 레이아웃 갯수가 많아지면 더 늘어나지 않을까 기대한다. 이 기능을 작성한 후 릴리즈로 속도 테스트를 해 보았다.

 

샘플비율 10% : 초당 76만개. CPU 점유율 30~60%

샘플비율 40% : 초당 21만개. CPU 점유율 90% -> 초당 36만개. CPU 약간 낮아짐

샘플비율 100% : 초당 12만개. CPU 100% -> 초당 22만개

 

샘플비율이 속도에 확실히 지대한 영향을 미친다. 걸러내는 기능을 넣었으니 샘플 비율 조정 기능을 빼 버릴까 했는데 그래도 두는 것이 나을 것 같다. 디폴트를 40% 정도로 조정하는 정도로 빨라진 속도를 조금 더 누리기로 한다. 샘플 비율에 따라 전체 순위도 영향을 받기 때문에 어느 정도 정확성을 확보해야 한다.

릴리즈에서 실행 결과를 보면 처음에는 느리고 CPU를 많이 점유하다가 뒤로 갈수록 걸러내는 비율이 높아지니 속도가 빨라지고 CPU 점유율도 점차적으로 낮아진다. 바람직한 변화이기는 하되 오히려 CPU 활용율이 떨어지는 것 같아 안타깝다. 일단 이대로 두되 CPU가 너무 논다 싶으면 배치를 늘리든가 다른 방법을 찾아 봐야겠다.

스레드에게 작업을 골고루 분배하기 위해 노는 스레드를 찾는 알고리즘을 약간 변형했다. 항상 0 ~ ThreadNum까지 찾다 보니 앞쪽 스레드에게 작업을 우선할당하는데 그러다 보니 앞쪽 스레드만 바쁜 것 같다. 그래서 현재 레이아웃 번호의 홀짝 여부에 따라 홀수인 경우는 후방에서 앞쪽으로 스레드를 찾도록 했다. 과학적 근거는 없고 이렇게 하면 괜히 골고루 분산될 거 같아서이다.

UI도 약간 조정했다. 현재 레이아웃 정보가 best 에디트 안에 있는 것이 보기 좋지 않아 위쪽 스태틱으로 빼고 스레드 개수와 연타 샘플은 트랙바 옆에 표시했다. 훨씬 더 보기 편하고 직관적인 것 같다.

여기까지 작업한 후 2층에 올라가 보니 dist3, 연타 샘플 10%에 대한 채점이 끝났다. dist4에 대해 EnumLayout을 돌리고 있는 중인데 현재 30조개 가량 세고 있다. 그래서 스레드는 10개밖에 사용하지 않았다.

133억개이며 8시간 3653초만에 초당 42만개의 속도로 작업을 끝냈다. 결과를 파일로 저장해 놓고 데스크탑에서 읽으니 다운된다. 분석해 보니 Score 멤버가 빠져 파일 포맷이 달라졌다. 이래서 구조를 바꾸면 안된다. Score 멤버 잠시 넣어 두고 읽거나 아니면 채점을 한 버전으로 읽어야 한다. 다음 결과를 얻었다.

 

  52,8032,3759   tenasohrilucdwmygvkpfbjxqz 감점9.25 15.45(14.22) 좌우50:50(0.0) 손가락17:20:22:39-42:26:25:06(0.6) 28:55:16(0.1) 지연7%(-0.9) 손연34%(-4.8) 연철0.0

  52,8032,1690   tenasohrilucdmwygvkpfbjxqz 감점9.37 15.45(14.22) 좌우50:50(0.0) 손가락17:20:22:39-42:26:25:06(0.7) 28:55:15(0.2) 지연7%(-1.0) 손연34%(-4.8) 연철0.0

  52,8032,3109   tenasohrilucdmgywvkpfbjxqz 감점9.55 15.45(14.51) 좌우50:50(0.0) 손가락17:20:22:39-42:26:25:06(0.7) 28:55:16(0.1) 지연7%(-1.0) 손연34%(-4.8) 연철0.0

 115,6017,1737   oentsarhidulmwcgybkpfvjxqz 감점9.90 15.44(11.84) 좌우49:51(0.0) 손가락18:20:24:35-42:25:26:06(2.7) 27:56:15(0.0) 지연7%(-0.6) 손연34%(-4.0) 연철0.0

 115,6017,1815   oentsarhidulmwcgpykbfvjxqz 감점10.01 15.45(14.84) 좌우49:51(0.0) 손가락17:21:24:36-42:25:26:06(0.6) 26:57:16(0.0) 지연7%(-1.4) 손연34%(-4.0) 연철0.0

  52,8032,8585   tenasohriluwdcmygvkpfbjxqz 감점10.28 15.45(15.39) 좌우50:50(0.0) 손가락17:20:22:39-42:26:25:06(0.6) 27:55:16(0.0) 지연7%(-0.9) 손연34%(-4.8) 연철0.0

 115,6016,9746   oentsarhidulmcwgpykbfvjxqz 감점10.37 15.45(14.84) 좌우49:51(0.0) 손가락17:20:25:36-42:25:26:06(0.6) 25:57:16(0.0) 지연7%(-1.0) 손연34%(-4.0) 연철0.0

 115,6017,1755   oentsarhidulmwcgyvkpfbjxqz 감점10.43 15.43(9.40) 좌우49:51(0.0) 손가락19:20:24:35-42:25:26:06(5.1) 27:56:16(0.0) 지연7%(-0.1) 손연34%(-4.0) 연철0.0

.....

  52,8032,7565   tenasohriluwdmycgvkpfbjxqz 감점16.99 15.45(15.39) 좌우49:51(0.9) 손가락17:19:22:40-41:25:26:06(1.9) 28:55:15(0.2) 지연7%(-0.2) 손연36%(-1.2) 연철0.0

  52,8031,7097   tenasohrilumdcfywbkgpvjxqz 감점16.99 15.46(18.84) 좌우50:50(0.0) 손가락16:20:22:40-42:26:25:06(0.6) 27:55:16(0.0) 지연8%(0.8) 손연35%(-3.3) 연철0.0

 115,6021,9873   oentsarhidmlucgfwykbpvjxqz 감점16.99 15.45(15.43) 좌우49:51(0.0) 손가락17:20:25:36-41:26:26:06(0.5) 26:57:16(0.0) 지연8%(0.5) 손연37%(0.6) 연철0.0

  52,8031,7338   tenasohrilumdwcygbkvfpjxqz 감점16.99 15.47(20.61) 좌우50:50(0.0) 손가락16:21:21:40-42:26:25:06(2.2) 27:55:16(0.0) 지연7%(-1.0) 손연34%(-4.8) 연철0.0

 

이 점수표를 분석해 보기 전에 오늘 작업한 최적화가 얼마나 개선되었는지 똑같은 조건으로 한번 더 돌려 보기로 했다. 시간은 빨라지고 결과는 같아야 한다. 노트북으로 다시 실행한 결과는 다음과 같다.

대충 봐도 결과가 같아 보이니 제대로 동작하고 있다. CPU13개를 사용하여 초당 86만개를 처리하여 4시간 16분에 작업을 끝냈다. 이렇게 빠른데도 CPU 사용율은 기대만큼 높지 않아 앞뒤 스레드 두 개만 맹렬히 돌고 나머지는 빈둥빈둥 놀고 있다. main에서 작업거리를 빨리 빨리 주지 못한다는 얘기다.

--------------------------------------------

dist4를 돌려 보기 전에 최적화와 잔손질이 좀 더 필요하다. 얼마든지 더 빨라질 수 있고 편의성도 높일 방안이 많고 버전 호환성도 향상시켜야 한다. 다음 작업을 추가로 더 진행하였다.

 

- 스레드 개수를 최대 2배까지 늘리도록 옵션의 범위를 조정했다. 당장 쓰든 안쓰든 옵션은 충분히 만들어 놔야 한다. 시스템이 정말 빠르다면 스레드 개수를 더 늘려서 활용할 수 있다.

- 배치의 개수를 조정한다. 현재 100개로 되어 있는데 메인보다 스레드가 빨라 노는 스레드가 많아지면 배치 크기를 늘리면 된다. 2개 단위로 2~200개까지 지정하도록 트랙바를 추가했다. 실행중에도 배치 개수를 바꿀 수 있도록 메인과 배치개수 변경 루틴을 뮤텍스로 감쌌다. 그리고 작업거리를 전달할 때 끝에 layout_id-1 표식을 남겨 스레드는 배치 개수에 직접적인 영향을 받지 않도록 했다.

이렇게 했지만 배치 개수에 따른 속도 변화나 CPU 점유율 변화를 직관적으로 느끼지 못하겠다. 2개일 때보다 10개일 때가 확실히 빠르지만 그 이상은 별 변화가 없다. 속도 테스트는 초당 32만개로 나오니 스레드 10개를 100% 활용하면 대략 300만개가 나와야 하는데 아직 그 속도에는 이르지 못한다. 메인의 대기 루틴을 좀 변경한 후 좀 더 테스트해 봐야겠다.

- 채점 구조체에 예비 멤버를 미리 선언해 두었다. Score가 있고 업고에 따라 저장 파일의 버전이 달라지는데 이런 변화는 앞으로도 발생할 수 있으므로 미리 예비 멤버를 선언해 두기로 한다. 이런 처리가 없으면 예전 채점 결과를 현재 프로그램으로 볼 수 없어 가용성이 떨어진다. 버전 관리까지는 필요 없고 그냥 충분한 예비 멤버를 만들어 두는 정도로만 했다.

- 도대체 왜 CPU를 다 활용하지 못하는지 순회 속도를 측정해 봤다. 스레드를 아무리 많이 써도, 배치 크기를 늘려도 별 차이가 없다. 100개씩 100만번 루프를 돌며 순회를 해 보니 초당 345만개이다. 이 정도 순회 속도면 충분하다. 그러나 스레드 대기, 배치 크기 변경 뮤텍스 대기, 순회중 작업거리 전달 등의 작업이 더 있어 이들을 넣어 보았다. 스레드 대기는 8개만 해 봤다.

 

 

스레드 대기

스레드 대기 안함

배치 뮤텍스 대기

92

221

배치 뮤텍스 대기 안함

111

345

 

스레드 대기 시간이 상당히 오래 시간을 잡아 먹는 셈이다. 배치 뮤텍스도 마찬가지로 꽤 시간을 잡아 먹는다. 역시 커널 객체는 비싸다. 둘 다 대기하면 초당 92만개이니 스레드에게 골고루 나누어줄만큼의 속도가 나오지 않는다. 결국 메인이 느리니 자원을 충분히 활용하지 못하는 사태가 벌어진다. 메인을 더 최적화해야 한다.

 

뮤텍스 대신 changeBatch 변수로 변경 : 339만개로 소폭 느려짐

뮤텍스 대신 크리티컬섹션으로 변경 : 342만개로 소폭 느려짐

memset 제거 : 377만개로 조금 빨라짐

memcpy 제거 : 별 차이 없음

 

크리티컬 섹션과 뮤텍스의 속도차가 이렇게 많이 나는지 몰랐다. 오히려 그냥 변수보다도 더 빠르다. dist2로 한바퀴 도는데 초당 56만개 140초가 걸렸는데 크리티컬 섹션으로 변경 후 초당 60만개 134초로 조금 단축되었다.

레이아웃을 찾을 때마다 채점표를 리셋하는 memset 호출문을 스레드로 옮겼다. 조금이라도 메인의 부담을 덜어주기 위해서이다. 초당 57만개 139초로 오히려 조금 느려졌는데 이건 잘 이해가 되지 않는다.

Best 배열에 대한 동기화도 뮤텍스 대신 크리티컬 섹션으로 교체했다. 스레드가 쓰는 시간이라 크게 의미는 없는 것 같지만 더 빠르다 하니 CPU 점유율을 낮추기 위해서라도 바꾸는게 좋을 것 같다. 초당 65만개 133초로 조금 더 빨라졌다.

샘플링 조건문도 빼 버렸다. 어차피 샘플링 기능도 필요 없는 것 같고 필요하면 주석 풀면 되니 메인 스레드의 조건에 굳이 있을 필요는 없을 것 같다. 속도상 별 차이는 없다.

메인 스레드를 이렇게까지 최적화를 해도 CPU 활용율이 나아지지 않고 30%대를 그대로 유지한다. 아무래도 작업 스레드를 대기하는 방식이 비효율적인 것 같다. 스레드쪽이 속도가 빨라져 메인이 대기하는 경우가 적어졌으니 아예 대기를 하지 말고 순번제로 돌리는게 어떨까 싶었다. 전에 이 방법을 쓰다가 대기하는 것으로 바꾸었는데 지금은 최적화가 많이 이루어져 괜찮을 것 같았다.

예전 백업 소스를 뒤져 0번부터 순서대로 순환하는 구조로 다시 만들어 보았다. 그러나 기대와는 달리 초당 40만개로 떨어지고 점유율도 30% 근방을 맴돈다. 스레드가 빨라졌어도 먼저 시작한 놈이 반드시 먼저 끝난다고 할 수는 없어 대기 시간이 오히려 더 많이 걸리는 것 같다. 이 작전은 또 실패다.

21521: 적합 점검 최적화, 리스트 뷰 출력

어제 main 최적화를 새벽까지 하다가 쓰러졌다. 다음날 출근을 해야 하니 맘대로 밤샘 작업을 할 수 없고 적당한 때에 잠을 자야 하는 강제가 생겨서이다. 오늘은 주말이고 더구나 월요일 휴가까지 내 놔서 시간이 넉넉하다. 이번에는 랭킹 프로그램을 완성하고 dist4까지 돌려볼 생각이다.

한번에 노는 스레드를 하나씩 찾지 한꺼번에 찾은 후 루프를 돌면 대기 시간을 줄일 수 있고 순회도 풀 스피드로 할 수 있다. 오늘 아침 출근하기 전에 문득 떠오른 생각인데 대기를 모아서 하므로 긍정적인 효과가 있을 것 같았다. 제어 구조를 다시 바꾸었는데 한방에 에러 없이 컴파일했다.

실행 결과 초당 57만개 138초걸렸으며 점유율은 50% 정도로 별다른 개선이 없다. 왜 그런가 싶어 디버그 버전에서 얼마나 대기하고 몇 개의 스레드를 찾는지 로그를 찍어 보았다.

 

0회 대기 후 1개의 스레드 찾음

148회 대기 후 1개의 스레드 찾음

9회 대기 후 1개의 스레드 찾음

0회 대기 후 4개의 스레드 찾음

0회 대기 후 2개의 스레드 찾음

 

대기를 하지 않을 때는 2개나 3개를 한꺼번에 찾기도 하지만 대기가 있을 때는 하나밖에 찾지 못한다. 메인이 노는 스레드를 못찾을 때 노는 스레드 하나가 생기는 즉시 찾기는 하는데 이 경우는 보통 하나밖에 없다. 다음 대기시에 n개의 스레드가 발견되기도 해 속도상의 개선이 분명히 있어야 하는데 이런 경우보다는 웬만해서는 하나밖에 찾지 못한다.

게다가 즉시 하나씩 찾는 것에 비해 모든 스레드의 신호 상태를 전부 조사해야 하니 손해보는 면도 있다. 메인의 순회 속도에 비해 작업을 기다리는 스레드의 속도가 느린 것 같아. 스레드 개수를 24개 최대치로 늘린 후 다시 로그를 찍어 보았다.

 

0회 대기 후 20개의 스레드 찾음

0회 대기 후 22개의 스레드 찾음

0회 대기 후 21개의 스레드 찾음

0회 대기 후 21개의 스레드 찾음

0회 대기 후 10개의 스레드 찾음

 

작업자가 많으니 메인이 대기하는 경우는 거의 없고 한번 찾을때 20개 가량의 스레드를 왕창 찾아내 루프를 돈다. 그러나 초당 59만개 135초로 전체 속도는 별반 차이가 없어 실망스럽다. CPU 점유율은 여전히 30% 수준밖에 안된다.

메인은 열심히 일거리를 퍼다 주는데 작업 스레드가 아무리 많아도 어디선가 병목이 생겨 즉각 즉각 처리하지 못하는 모양이다. 이제 의심 가는 부분은 arBest에 대한 동기화 부분밖에 없다. 베스트가 깨지든가 말던가 크리티컬 섹션으로 보호하는 부분을 빼고 전속력으로 작업을 처리하도록 해 봤다. 그래 봤자 134초로 거의 차이가 없어 이 부분은 아닌 것 같다.

아예 베스트에 대한 집계를 빼 버리고 전속력으로 채점만 하라고 해 봤더니 속도는 초당 30, 39초로 거의 절반으로 줄어든다. 베스트 집계 시간은 아끼지만 최저 점수를 갱신하지 않으니 모든 레이아웃에 대해 연타를 점검하며 그러다 보니 오히려 더 느려지는 것이다. 대신 CPU 이용율은 80%까지 치솟는데 이건 쓸데없는 짓을 하고 있어서이다.

결국 작업 스레드보다는 메인 스레드가 순회하고 작업을 지시하는 속도가 느린 것이다. 초당 100만개 정도밖에 유효 레이아웃을 찾지 못하는 것 같다. 그래서 TestInDist 점검을 true로 바꾼 후 돌려 봤다. CPU는 거의 90~100%에 근접하고 초당 500만개를 채점해 내기는 한다. 그러나 이는 무효한 것까지 채점하니 연타 무효가 많아 개수만 늘어난 것이며 원하는 바도 아니고 전체 개수가 늘어나 의미 없다.

TestInDist가 과연 얼마나 느린지 다시 속도를 측정해 봤다. 8개 스레드 신호 상태 조사, 크리티컬 섹션 대기까지 해서 초당 108만개를 순회한다. 여기서 TestInDist 호출을 생략하고 true를 주니 초당 처리 개수가 1376만개로 늘어나니 이 메서드가 시간을 엄청나게 잡아 먹고 있다. 코드를 보자.

 

        for (int idx = 0; idx <= LAST; idx++) {

                    // 대응 위치의 문자끼리 빼서 dist 이상 거리인지 조사.

                    if (ABS(arLayoutInt[idx] - oriLayoutInt[idx]) > Dist) return false;

        }

        return true;

 

24개 문자에 대해 원본 문자와의 거리가 Dist 이상인지 다 점검하니 시간이 걸릴 수밖에 없다. 제일 먼저 수정한 건 순회 방향을 역순으로 바꾸는 것이다. 뒷부분의 변화가 잦아 뒤에서 부적합이 나올 확률이 높으니 가급적 먼저 점검하는 것이 유리하다.

 

for (int idx = LAST; idx >= 0; idx--) {

 

이 수정에 의해 초당 124만개로 속도가 개선되었다. 다음은 ABS 매크로가 삼항 조건 연산자로 되어 있는데 이를 풀어 썼다.

 

if (arLayoutInt[idx] - oriLayoutInt[idx] > Dist || arLayoutInt[idx] - oriLayoutInt[idx] < -Dist) return false;

 

두 조건으로 풀어 쓰면 쇼트 서키트가 적용되어 좀 더 빠르지 않을까 기대했는데 결과는 같았다. 최후 부적합 하나에 대해서만 쇼트 서키트가 적용되니 별 효과가 없다. 다음은 -Dist를 미리 mdist 변수에 대입해 놓고 비교해 봤는데 역시 효과 없다. 뺄겜한 결과를 임시 변수 t에 대입해 놓고 비교했는데 이 또한 효과가 없었다. 아마 최적화되면 두번 뺄셈을 하지 않을 것이다.

다음은 oriLayoutInt[idx]가 곧 idx와 같다는 점에 착안하여 배열 참조문을 없애고 idx와 바로 비교했다. 그러고 보면 oriLayoutInt 자체는 i를 주고 i를 리턴받는 룩업이어서 별 쓸모가 없다. arLauoutInt의 첨자와 내용물 사이의 거리를 재면 된다. 아예 oriLayoutInt 배열을 없애 버렸다. 이 변화에 의해 속도가 빨라져야 하는데 125만개로 근소하게만 빨라졌다.

다음으로 TestInDist 함수를 아예 코드안에 박아 넣어 버렸다. 아무리 inline이라고 해도 스택 프레임을 만들고 리턴을 하니 호출 부담이 있다. 함수의 코드를 내장하고 측정하니 129회로 조금 더 빨라졌다. 더 이상의 최적화는 어렵다. idx 변수를 register 기억 부류로 지정해도 별 변화가 없고 생략할 코드도 더 보이지 않는다.

그래도 여기까지 최적화한게 꽤 효과가 있어 초당 82만개로 119초만에 dist2 채점을 끝냈다. 노트북에서 스레드 13개로 돌리니 초당 84만개로 17초 걸렸다. 스레드가 많아도 둘다 4GHz 클럭이어서 별반 차이는 없는 셈이다.

스레드나 배치 크기를 최대치로 늘리니 초당 90만개 13초에 끝을 내기는 하지만 들이는 자원에 비해 큰 효과는 없다고 할 수 있다. 결국 스레드의 개수는 중요치 않고 클럭이 높아야 하며 메인 스레드를 얼마나 최적화하는지가 속도의 관건이다. 메인은 더 쪼갤 수도 없어 여기까지가 일단은 한계인 것 같다.

------------------------------

최적화는 대충 이 정도로 해 두고 자잘한 기능을 좀 개선했다. 채점중에 일시 정지해 두면 경과 시간은 계속 흘러가는데 정지한 시간을 따로 계산해 빼 주었다. 초당 처리 개수 계산을 위해 스레드 개수 변경시 시작 id를 리셋했는데 그럴 필요 없이 결과 시간만 정확하게 계산하면 된다.

start_id라는 불필요한 변수는 삭제하고 좀 더 정확한 시간 계산 방법을 도입했다. 샐행중에 스레드 개수나 배치 크기를 변경해도 별 이상없이 잘 동작하며 시간 계산도 정확하다. CPU가 열받아 잠시 쉬어야 하는 경우도 있으니 일시 정지 기능도 정확하면 좋다.

다음은 베스트 출력을 리스트 뷰로 변경했다. 에디트에 출력하면 보기도 좋지 않고 정렬도 안되어 깔끔하게 출력하고 정렬을 통해 점수 변별력을 쉽게 조정하기 위한 편의 기능이다. Win32 코딩 그만큼 해 봤으니 리스트 뷰 정도야 쉽게 사용할 수 있다. ApiExam 참고하니 출력은 금방 처리했다.

에디트는 더 이상 필요 없으니 없애 버릴까 하다가 클립보드에 결과를 복사하기 편리하니 그냥 냅 뒀다. 에디트란은 위쪽에 좁게 두고 아래쪽에 리스트 뷰를 배치하고 여기다 11개의 컬럼을 나누고 결과를 출력했다. 훨씬 더 보기 좋고 소수점 이하 두 자리까지 정밀도도 높였다.

헤더를 누르면 정렬하는 기능도 작성했는데 이게 예상외로 난점이 좀 있었다. 정렬을 위한 기준키가 필요한데 LPARAM 32비트에 저장 가능해야 하므로 64비트의 id나 문자열 형태의 layout은 안된다. 32비트 범위내의 id로만 구현하자면 가능은 하지만 dist3 이상은 다룰 수 없다는 문제가 있다. 고민하다가 결국 순위를 기준키로 사용했는데 0~999까지라 별 문제 없는 것 같다.

정렬도 무조건 문자열 순서로 하다 보니 숫자 포맷과는 맞지 않다. 문자열에서 괄호안의 감점을 읽어 이 순서대로 정렬했다. 포인터 연산해서 실수값 위치를 찾기는 했는데 그래봤자 문자열이라 순서가 잘 맞지 않다. 이대로도 볼만해 그냥 둘까 하다가 찜찜해서 결국 실수로 바꾼 후 비교했다. 위 그림은 손가락 연타 기준으로 정렬한 것이다. 더블클릭하면 레이아웃을 보여주는 기능도 작성했는데 FULLROWSELECT 상태에서는 클릭으로 활성화가 되지 않는다.

이 작업을 하는데는 대략 두어시간 걸린 것 같은데 다 아는 내용이지만 함정이 몇 개 있고 정렬이나 포맷팅 등에 좀 귀찮은 면이 있다. 이런거 잘 하려면 역시 경험이 풍부해야 하고 시간이 넉넉해야 하며 체력이 받쳐 줘야 한다. 순위도 문자열 정렬하여 순서가 맞지 않는데 딱히 불편하지 않으니 그냥 이대로 두기로 한다. 여기까지 작업한 후 중간 성능 점검을 해 봤다.

 

데탑 : CPU 50%, 초당 72만개 118

TUF : CPU 50%, 초당 83만개 18

요가 : CPU 70%, 초당 47만개 159

 

초당 83만개면 사실 싱글 스레드로 개수만 세는 속도와 거의 비슷하다. 메인 스레드를 더 최적화하지 못해 좀 아쉬운 감이 있는데 최적화를 위해 내 시간을 너무 많이 축내고 있다. 이제 코드는 안정화 정도만 하고 최적화 그만 하자.

---------------------------------------

dist3를 노트북에서 다시 돌렸다. 3시간 14분만에 초당 114만개를 채점해 냈다. 저장 및 읽기 기능이 제대로 동작하고 있어 데스크탑에서도 결과를 볼 수 있다. 그러나 결과만 잘 읽을 뿐 프로그램 상태가 STOP이어서 상세 점수를 볼 수 없고 32비트를 초과하는 id를 추출해내지 못하는 문제가 있었다.

파일에서 결과를 읽은 후 statusPAUSE로 변경하여 결과를 볼 수 있도록 했으며 id를 추출할 때 atoi가 아닌 _atoi64 함수를 대신 사용하여 64비트의 id를 읽도록 했다. C 런타임도 64비트 함수는 제대로 다 구현되어 있어서 다행이다.

이걸로 점수 변별력을 다시 한번 더 점검해 보자. 감점의 범위는 9.25~19.29에 걸쳐 대략 10점 차이가 난다. dist2의 감점 범위가 25.04~35.01이니 dist2의 모든 레이아웃은 dist3에서는 베스트 1000에 명함도 못 내미는 셈이다. 그렇다면 dist4로 다시 돌려 보면 dist3의 모든 배열도 마찬가지가 되지 않을까 싶다. 그래서 dist4를 빨려 돌려 봐야 하며 최종 점검은 dist4로 하게 될 것 같다.

그전에 dist3을 기준으로 채점이 과연 합리적인지 점검하고 다시 한번 더 조정하기로 한다. 1등한 배열이 기준 빈도에 비해 얼마나 멀어졌는지 비교해 보자. 한두칸씩 이동한 경우가 많고 IV, K는 세칸 이동했다.

 

기준빈도 :    ETAONISHRDLUMCWYGFPBVKJXQZ

1:            TENASOHRILUCDWMYGVKPFBJXQZ

 

1등과 1000등의 점수를 비교해 보자.

 

tenasohrilucdwmygvkpfbjxqz

부담도 15.45. 14

좌우 50:50. 0

17:20:22:39-42:26:25:06 0.6

28:55:16. 0.1

지연타 : 7%. -0.9

손연타 : 34%. -4.8

연철 감점 : 0

감점 9.25

 

oentiarsdhmlucwgpykbfvjxqz

부담도 15.44. 11

좌우 51:49. 0

16:20:24:38-38:27:27:06 0.5

25:57:17. 0

지연타 : 10%. 6.2

손연타 : 37%. 0.7

연철 감점 : 0.5

감점 19.29

 

부담도는 약간의 차이가 있으며 3점 정도 벌어지니 나쁘지 않은 변별력이다. 좌우 분담율은 둘 다 만점 범위 안이고 손가락, 행 분담율, 연철은 거의 영향을 머치지 않는다. 가장 큰 차이는 연타인데 지연타 3% 차이로 7점 정도 벌어졌고 손연타 3% 차이로 5.5점 벌어졌다. 부담도가 좋아도 연타로 인해 너무 많이 벌어진다.

dist3에서 각 기준별로 점수의 범위를 조사해 보았으며 현재 채점 기준으로 변별력이 얼마나 있는지도 점검해 보았다. 133억개중 1000개를 뽑은 것이어서 변별력이 좁아 이들끼리 차별화를 두드러지게 해야 한다. 또한 각 기준별로 밸런스도 조정할 필요가 있다.

 

기준

현재 범위-최소,최대(범위)

기존-만점,허용,배수(변별)

수정

부담도

15.39~15.45(0.06)

15.4, *300(18)

*300 -> *500(30)

좌우

49.15~54.46(5.31)

52, +-2, 초과제곱

 

0.04~6.27(6.23)

각비율, +-3, 초과제곱

 

0~2.23(2.23)

각비율, +-3, 초과제곱

 

연철

0~3(3)

점수대로

 

지연타

7.41~12.98(5.57)

8, *3(16.71)

7, *2(11)

손연타

34.59~42.45(7.86)

37, *2(15.72)

34, *1(7.86)

 

부담도에 좀 더 변별력을 주기 위해 배수를 300에서 500으로 늘렸다. 베스트끼리 최대 30점만큼 구분할 수 있다. 지연타와 손연타는 마이너스 감점이 나오지 않도록 만점을 약간씩 내리고 배수를 3에서 2배로, 2배에서 1배로 각각 내려 너무 많은 영향을 미치지 않도록 조정했다.

연타를 점검하는 조건이 현재 최대 감점보다는 많을 때 여기에 4점을 미리 감점한 조건을 적용한 후 각각 2점씩 감점하여 시간을 절약하는 코드가 있다. 베스트에도 못 들거면 아예 제외한다는 작전인데 후반에는 이 조건이 맞을 수 있지만 전반부에 샘플이 많지 않을 때는 맞지 않을 수도 있다. 점검 개수가 천만개 이상일 때만 이 조건을 점검하되 +4 미리 감점은 제거하여 초반부에는 모든 레이아웃의 연타를 다 보는 것으로 수정한다. 건너 뛰더라도 감점을 1점 정도만 가정하도록 했다.

1등이나 1000등이나 오른손의 손가락 분담률이 오른차순이 아니다. 중지보다 약지의 분담률이 더 높게 나타나는데 이는 약지에 Enter키가 배정되어 있기 때문이다. 315일 기록을 보면 Enter를 오른 약지에 놓되 확정한 것이 아니며 BS랑 변경할 가능성도 있다. 아직 결정되지도 않은 정책을 미리 적용해 놓으니 알파벳 배열을 채점하는데 왜곡이 발생한다. Enter을 일단은 오른 엄지에 배치해 놓고 채점하기로 한다.

 

{'\r',4, 7, 7, 2, 30},

 

arCharInfo 배열의 '\r' 에 대한 컬럼, 손가락이 원래 11번이었는데 엄지인 7번으로 변경했다. 나중에 다시 수정하는 한이 있더라도 알파벳 배열에 Enter의 위치까지 고려할 필요는 없을 거 같다. 이렇게 되면 오른손가락 분담률도 조정할 필요가 있다. 현재 42:24:24:10으로 되어 있다.

샘플 문서상 Enter의 발생 빈도가 2% 정도인데 오른손만으로 보면 대략 4%를 차지했던 셈이다. 이 비율이 약지에서 빠지면 중지 부담을 더 높게 잡아야 한다. 집게도 약간 높은 거 같으니 여기서 1 빼고 집게에 3 더하고 약지에 2를 빼 41:27:22:10으로 만점 비율을 설정한다.

여기까지 채점 비율과 키보드 정보 등을 수정한 후 dist2에 대해 다시 채점을 해 보았다. 기존 채점 결과와 비교해 보고 논리가 제대로 수정되었는지 확인하기 위해서이다. 채점 기준이 바뀌니 순위도 왕창 바뀌어서 기존의 베스트 상위권이 거의 다 사라지고 없다.

감점 범위가 25~35에서 27~37로 평행 이동했다. 연타율의 만점을 조정해서 그런 것 같다. 손가락 분담율은 Enter키를 치웠더니 오른쪽도 오름차순으로 잘 정렬되어 있어 보기 좋다. 손가락 연타 비율이 10%~16%로 증가했는데 연타는 알파벳만 추린 후 하는 것이어서 Enter가 빠진 것과는 상관이 없다.

연타율이 낮아도 베스트 선정에 영향력을 덜 미쳐 높은 연타율이 많이 포함된 것 같다. dist2라 연타 감점이 너무 높게 나타나는 것 같은데 더 정확한 평가는 dist3을 돌려 봐야 알 것 같다. 다시 돌리는데 대략 3시간 걸리니 결과를 보고 밸런스 조정을 한번은 더 해야 할 것 같다.

 3시간에 걸쳐 dist3에 대해 채점을 돌렸다. 감점 범위는 11~19까지이며 1등의 손가락 연타율이 12%, 손연타는 37%로 계산된다. 부담도는 15.39~15.40이며 감점은 -7~2점 범위이다. 빈도순보다 더 낮은 감점을 보이는 배열이 많다. 손가락 연타 범위는 11~14%이며 감점 범위는 8~14점이다. 손연타는 37~45%이며 감점 범위는 3.9~11.3이다.

어차피 같은 기준 내에서는 상대 평가이지만 연타율의 감점이 너무 높게 반영되는 것 같다.

21527: dist4 채점 완료, 점수 조정

주말에 어디 놀러 갔다 오고 평일에는 회사 다니느라 노트북에게 일만 시켜 놓았다. 이왕 돌리는 김에 dist4도 같이 돌렸다. 중간 중간에 올라가 확인해 보니 나름 잘 진행되고 있었다.

 

5/24Enum 54/7188. Ranking 270037시간 초당 198

5/26Enum 62/8064. Ranking 534081시간 초당 181

 

개수만 세는 놈보다 채점하는 놈이 훨씬 더 빠르다. 이럴거 같으면 아예 셀 필요 없이 바로 채점을 해도 될 거 같다. 속도를 조금 더 개선하면 dist5까지도 뽑아볼 수 있을 거 같다. 일단 dist4까지 뽑아서 점수 변별력을 좀 더 조정한 후 결정해야겠다. 그 전에 몇 가지 개선 사항을 작업했다.

 

- 내림차순 정렬은 불필요하다. 항상 오름차순으로만 정렬하는 것이 오히려 더 보기 편하다. 범위를 아는게 목적이므로 범위도 같이 보여 주면 더 좋을 것 같다. 리스트 위에 스태틱하나 배치하고 정렬시 범위를 표시했다.

- 유효한 레이아웃의 갯수만 세는데 총 레이아웃 갯수도 같이 표시하면 좋을 것 같다. 건너 뛰더라도 얼마나 진행했는지는 알아야 한다. 별 쓸 데는 없지만 일단 같이 카운팅하기로 한다.

총 개수를 찍어 보니 main이 왜 바쁜지, 최적화를 그렇게 해도 CPU 활용율이 좀체 향상되지 않는지 알 수 있다. 73억개 중에 5600만개를 세고 있으니 main은 채점 개수의 100배를 순회하고 있는 셈이다. dist4를 돌려 보니 뒷부분에는 부적합 배열이 많아 CPU가 노는 경우가 많은데 중간에라도 가급적 건너 뛰어야 한다. 순회 알고리즘이 좀 더 정교해져야 할 거 같다.

- EnumLayout이 별도의 프로그램으로 분리되어 있는데다 콘솔 프로그램이어서 사용하기도 불편하다. 그래서 Ranking에 통합할려고 생각했는데 관두기로 했다. 앞으로도 계속 사용하려면 정리해 둘 필요가 있지만 v4v5만 구하면 더 이상은 쓸 일이 없어 Ranking 프로그램만 지저분해진다.

채점과 별 속도 차이가 없고 멀티 스레드를 돌리는 채점이 오히려 더 빠르다. 이 차이는 정수로 순회하고 뒷부분부터 적합 배열을 순회하는 알고리즘때문이기도 하다. EnumLayout만 이 알고리즘으로 수정해 두었는데 이전 버전과 비교해 보니 딱 2배 빠르다. 2층의 노트북이 2주째 v4를 세고 있는데 알고리즘만 수정해도 1주만에 끝낼 일이었던 셈이다. 채점은 1주일이면 끝날 거 같다.

다행히 앞쪽 교환시 부적합 배열을 건너 뛰는 최적화는 수행했는데 뒤쪽도 그럴 수 있지 않을까 하여 좀 연구해 봤는데 이건 방법이 없는 것 같다. 앞쪽은 한번 부적합이면 뒤쪽이 모두 부적합이어서 통째로 건너뛸 수 있지만 뒤쪽은 당장 부적합해도 교환이 일어나면 다시 적합해질 수 있어 함부로 건너뛸 수 없다.

22개 문자에 대해  22!11해이다. 앞쪽 건너뜀으로 인해 dist에 따라 70, 1, 70조 정도로 개수가 줄어들어 최소한 억단위의 시간을 단축했다. 뒷부분은 다 건너 뛴다고 해도 100배 정도밖에 차이가 나지 않으니 앞쪽 건너뜀으로 만족해도 될 거 같다. 어차피 건너뛰는 건 채점은 하지 않으니 스레드가 좀 놀 뿐 그다지 손해보지도 않는다.

시간을 더 투자해서 정밀하게 최적화를 더 할 수 있지만 내가 그 시간을 쓰는 것보다는 컴퓨터를 좀 더 부려 먹는 것이 더 낫다. 이 정도 최적화에 투자했으면 할만큼 했으니 이후에는 점수나 잘 조정해서 딱 맞는 배열을 찾아 보자. 더 많은 시간이 필요하다 하더라도 다음에 또 조정하면 되니 dist5 정도에서 최적 배열을 취하는 것도 나쁘지 않은 것 같다.

------------------------------------------

드디어 보름에 걸친 dist4 개수 세기 작업이 끝났다. 결과는 다음과 같다.

 

개수 : 70753996047980. 유효 레이아웃 : 861543171080

총 계산 시간 : 22576= 15.6

 

그보다 늦게 시작한 채점도 끝이 났다. 개수는 단 한개도 틀리지 않고 똑같이 나왔으며 경과 시간은 152시간 10분이니 6일하고도 8시간이 걸렸으며 초당 157만개를 채점했다. Enum의 알고리즘이 최적화되지 않았고 싱글 스레드로 돌렸다고는 하지만 3배나 더 느렸다는게 좀 의아하다. 이제 Enum은 더 돌릴 일 없이 Ranking만 돌리면 될 거 같다. 기준을 바꿔서라도 6일 정도면 딱 일주일이니 충분히 기다릴만하다.

Enum이 콘솔에 5000만개 단위로 출력한 중간 결과를 검토해 보자. 전체 개수는 꾸준히 5000만개씩 늘어나지만 유효 레이아웃 개수는 늘어나다가 말다가 한다. 408716번 찍혀 있는데 이는 8억개 중에 유효한 레이아웃이 없었다는 얘기이다. 만단위 이하에서 혹시 있을 수도 있지만 그럴거 같지는 않다.

 

num=703041 0000 OK=8606 2614 layout=efgabcilmjkdnqohptsruv : PASS

num=703041 5000OK=8606 2997 layout=efgabcjdhmoinpqsrvutlk : PASS

num=703042 0000 OK=8606 3418 layout=efgabcjdinlporhtkumvqs : PASS

num=703042 5000 OK=8606 3738 layout=efgabcjdlhmpknitusrovq : PASS

num=703043 0000 OK=8606 4013 layout=efgabcjdminkhqpsrtvulo : PASS

num=703043 5000 OK=8606 4087 layout=efgabcjhdlnmqiokrptvsu : PASS

num=7030440000 OK=8606 4087 layout=efgabcjhimlkpqotnvrdus : PASS

num=703044 5000 OK=8606 4087 layout=efgabcjhknipqdosmrvtlu : PASS

num=703045 0000 OK=8606 4087 layout=efgabcjhmdlokqsprtivnu : PASS

num=703045 5000 OK=8606 4087 layout=efgabcjidknhqmrsotvlpu : PASS

num=703046 0000 OK=8606 4087 layout=efgabcjihlkpnmdsutvqor : PASS

num=703046 5000 OK=8606 4087 layout=efgabcjikmhndpqosvutrl : PASS

num=703047 0000 OK=8606 4087 layout=efgabcjilnodmqsphutrkv : PASS

num=703047 5000 OK=8606 4087 layout=efgabcjkdilhmqsotrunvp : PASS

num=703048 0000 OK=8606 4087 layout=efgabcjkhldoqmrsuitvpn : PASS

num=703048 5000 OK=8606 4087 layout=efgabcjkimhlpdntsuqvor : PASS

num=703049 0000 OK=8606 4087 layout=efgabcjkmdihonpqustrvl : PASS

num=703049 5000 OK=8606 4087 layout=efgabcjldiomkrnqshtpuv : PASS

num=703050 0000 OK=8606 4087 layout=efgabcjlhmdipkqntsorvu : PASS

num=703050 5000 OK=8606 4087 layout=efgabcjlinhomkqsdrupvt : PASS

num=703051 0000 OK=8606 4087 layout=efgabcjlmhikornpsdvqtu : PASS

num=703051 5000 OK=8606 4366 layout=efgabckdhlonimrtuvsjpq : PASS

num=703052 0000 OK=8606 4746 layout=efgabckdimnpohqtrvsjlu : PASS

num=703052 5000 OK=8606 5035 layout=efgabckdjnmpolsruhqvit : PASS

num=703053 0000 OK=8606 5286layout=efgabckdmihlpnotsqjuvr : PASS

num=703053 5000 OK=8606 5373 layout=efgabckhdljmirsonvpqut : PASS

num=703054 0000 OK=8606 5373 layout=efgabckhimdnprqltjvosu : PASS

num=703054 5000 OK=8606 5373 layout=efgabckhjndlqpstivurom : PASS

num=703055 0000 OK=8606 5373 layout=efgabckhmdjpiqnorvlstu : PASS

num=703055 5000 OK=8606 5373 layout=efgabckidjnhlosqmvptru : PASS

num=703056 0000 OK=8606 5373 layout=efgabckihlmdoqsjrtpvnu : PASS

num=703056 5000 OK=8606 5373 layout=efgabckijmldnhoprqvtus : PASS

num=703057 0000 OK=8606 5373 layout=efgabckimdhopjlqutnsvr : PASS

num=703057 5000 OK=8606 5373 layout=efgabckjdimpnqrhutosvl : PASS

num=7030580000OK=8606 5373 layout=efgabckjhlmonqstduvrip : PASS

num=703058 5000 OK=8606 5373 layout=efgabckjimnodhlqprstuv : PASS

num=703059 0000 OK=8606 5373 layout=efgabckjmdnhpolqitruvs : PASS

num=703059 5000 OK=8606 5373 layout=efgabckldjiompsrhutnvq : PASS

num=703060 0000 OK=8606 5373 layout=efgabcklhmniojrpqutsdv : PASS

num=703060 5000 OK=8606 5373 layout=efgabckljdiohprntmqsvu : PASS

num=703061 0000 OK=8606 5373 layout=efgabcklmihjqprodsvtun : PASS

num=703061 5000 OK=8606 5822 layout=efgabdchimolkqsrutvjnp : PASS

num=703062 0000 OK=8606 6347layout=efgabdchjnmkpiotlrvsuq : PASS

num=703062 5000 OK=8606 6814layout=efgabdchlimnporjustvqk : PASS

num=703063 0000 OK=8606 7226 layout=efgabdchmjlknrpouqstiv : PASS

num=703063 5000OK=8606 7701 layout=efgabdcihlmjknpquvsotr : PASS

num=703064 0000 OK=8606 8208 layout=efgabdcijmhpkonsrtluqv : PASS

num=703064 5000 OK=8606 8553 layout=efgabdcikmopnlhtursvjq : PASS

num=703065 0000 OK=8606 8859 layout=efgabdcilnophjksmrqtuv : PASS

num=703065 5000 OK=8606 9235 layout=efgabdcjhionprmqkltsvu : PASS

num=703066 0000 OK=8606 9750 layout=efgabdcjiknmhrosqlvput : PASS

num=703066 5000 OK=8607 0101 layout=efgabdcjklmhpriqtsuvno : PASS

...........................

 

num=707514 5000 OK=8615 4317 layout=efghickabmdpjqolrvnust : PASS

num=707515 0000 OK=8615 4317 layout=efghickaljomqpbndvusrt : PASS

num=707515 5000 OK=8615 4317 layout=efghickdaljnqpbsrtvoum : PASS

num=707516 0000 OK=8615 4317 layout=efghickdlanbqmojpursvt : PASS

num=707516 5000 OK=8615 4317 layout=efghickjambdnopsuqltvr : PASS

num=707517 0000 OK=8615 4317 layout=efghickljmabpnqsturvod : PASS

num=707517 5000 OK=8615 4317 layout=efghidabcnlpojskmvrtuq : PASS

num=707518 0000 OK=8615 4317layout=efghidabkcloqpjmtvunsr : PASS

num=707518 5000 OK=8615 4317 layout=efghidabljcpqnoktsvrum : PASS

num=707519 0000 OK=8615 4317 layout=efghidabmkcoqlsjtrvupn : PASS

num=707519 5000 OK=8615 4317 layout=efghidajbmcnkqrpluvtos : PASS

num=707520 0000 OK=8615 4317 layout=efghidajlknbcroputqvms : PASS

num=707520 5000 OK=8615 4317 layout=efghidakblcmjqnpruvsto : PASS

num=707521 0000 OK=8615 4317 layout=efghidaklbjonqsmtcurvp : PASS

num=707521 5000 OK=8615 4317 layout=efghidalbjkmnrqospuvtc : PASS

num=707522 0000 OK=8615 4317 layout=efghidaljnbpkcromtsuqv : PASS

num=707522 5000 OK=8615 4317 layout=efghidcabjnkqlmruptvos : PASS

num=707523 0000 OK=8615 4317 layout=efghidcajklpmroqnvubst : PASS

num=707523 5000 OK=8615 4317 layout=efghidcaklbojnmpqtvurs : PASS

num=707524 0000 OK=8615 4317 layout=efghidcalmbjoksqrtupvn : PASS

num=707524 5000 OK=8615 4317 layout=efghidcamnbkorsjupvqtl : PASS

num=707525 0000 OK=8615 4317 layout=efghidcjanolqmrstuvbkp : PASS

num=707525 5000 OK=8615 4317 layout=efghidcjmalpqkbnoutsrv : PASS

num=707526 0000OK=8615 4317 layout=efghidckamolbpsrjuvtqn : PASS

num=707526 5000 OK=8615 4317 layout=efghidcklnapojrmbsqvut : PASS

num=707527 0000 OK=8615 4317 layout=efghidclambkqpjnstruov : PASS

num=707527 5000 OK=8615 4317 layout=efghidclkjonmrsquvpabt : PASS

num=707528 0000 OK=8615 4317 layout=efghidjablkpqmncsvoutr : PASS

num=707528 5000 OK=8615 4317 layout=efghidjalbncpkqrutvmos : PASS

num=707529 0000 OK=86154317 layout=efghidjkalnobpmqsurtvc : PASS

num=707529 5000 OK=8615 4317 layout=efghidjlkanbcrsopqmutv : PASS

num=707530 0000 OK=8615 4317 layout=efghidkabnjplmcqtrusvo : PASS

num=707530 5000 OK=8615 4317 layout=efghidkambcplqorjtnvsu : PASS

num=707531 0000 OK=8615 4317 layout=efghidkjmabnclqprvotsu : PASS

num=707531 5000 OK=8615 4317 layout=efghijabcdokmrqntslvup : PASS

num=707532 0000 OK=8615 4317 layout=efghijabkmclndosqrvtpu : PASS

num=707532 5000 OK=8615 4317 layout=efghijabmnkoqclrtsdvpu : PASS

num=707533 0000 OK=8615 4317 layout=efghijadkbmocqrsnlvtpu : PASS

num=707533 5000 OK=8615 4317 layout=efghijadmklbpqcntursov : PASS

num=707534 0000 OK=8615 4317 layout=efghijalbcdpmrkqtouvsn : PASS

num=707534 5000 OK=86154317 layout=efghijcabknlmdqorsvutp : PASS

num=707535 0000OK=8615 4317 layout=efghijcaknmpbdqsrvtuol : PASS

num=707535 5000OK=8615 4317 layout=efghijcdabnkoqslrpvutm : PASS

num=707536 0000 OK=8615 4317 layout=efghijcdklnoampstrqvub : PASS

num=707536 5000 OK=8615 4317 layout=efghijcdmnakqlbptrosvu : PASS

num=707537 0000 OK=8615 4317 layout=efghijclakbdpqsnrmovut : PASS

num=707537 5000 OK=8615 4317 layout=efghijdablmncpqtuskrov : PASS

num=707538 0000 OK=8615 4317 layout=efghijdalbnmqrctousvkp : PASS

num=707538 5000 OK=8615 4317 layout=efghijdkalompqsrnubtvc : PASS

num=707539 0000 OK=8615 4317 layout=efghijdlkanpqmotbusrcv : PASS

num=707539 5000 OK=8615 4317 layout=efghijkdabcolmrqnsvtpu : PASS

num = 70753996047980, ok = 861543171080, 시간=22576 36

 

 

제일 마지막에는 86154317만이 716억개를 세는 동안 계속 반복되었다. 초당 1억개씩 센다고 해도 대략 12분 정도 유효한 배열이 없었다는 얘기이다. 뒤로 갈수록 이런 현상이 점점 많았을 것으로 추측되며 그러다 보니 메인 스레드만 바쁘고 작업 스레드는 할 일 없이 펑펑 놀고 있을 수밖에 없다.

무효한 배열을 건너 뛰는 알고리즘을 찾지 않는 한 이는 어쩔 수 없는 것 같은데 찾아도 완벽하지 않을 거 같다. 70조개 중 8600억개가 유효하니 그래도 100배는 안되는 셈이다. 이건 그냥 건너 뛰게 내 버려 두는게 나을 것 같다. 메인 혼자 바쁘니 CPU도 과열되지 않고 말이다.

dist4의 채점 결과를 보자. 현재 최적화 수준으로 dist3133억개는 3시간이면 채점 완료한다. dist4는 그보다 64배 더 많은 8600억개이며 시간도 비례로 늘어 6일 조금 더 걸렸다. 이 비율대로라면 dist5는 대략 55조개에 달하며 딱 1년 정도 시간이 든다는 얘기다. 이건 뭐 당장은 아예 시도도 못해볼 것 같다. 이 방법보다는 dist4의 베스트1000에 대해 하나씩 변형을 가해 보는 것이 더 나을 것 같다.

그럼 이제 채점 결과를 보자. 점수 분포는 7.23 ~ 13.24까지이다. dist3의 분포가 11~19까지이므로 겹치는 부분이 약간 있는 것 같다. 정확하게 있는지는 채점 기준이나 UI가 좀 바뀌어 현재 프로그램으로 다시 채점해 봐야 하는데 그럴 필요까지는 없을 것 같다. 1등을 한 배열은 다음과 같다.

 

teoanirshculdwmfpvkygbjxqz

부담도 15.38. -7.58

좌우 52:48. 0

17:20:24:36-43:28:20:07 0.8

28:56:15. 0

지연타 : 11.02%. 8

손연타 : 37%. 3.9

연철 감점 : 2

감점 7.58

 

부담도에서 마이너스 감점이 높게 나와 연타의 감점을 상쇄하고도 좋은 점수를 받았다. 베스트 1000의 점수 분포는 다음과 같다.

 

기준

현재 범위-최소,최대(범위)

기존-만점,허용,배수(변별)

수정

부담도

15.38~15.42(0.04)

15.4, *500(20)

만점을 15.38

좌우

52.66~55.90(3.25)

52, +-2, 초과제곱

 

0.04~6.27(6.23)

각비율, +-3, 초과제곱

 

0.02~2.95(2.97)

각비율, +-3, 초과제곱

 

연철

0~2(2)

점수대로

 

지연타

6.97~12.83(5.86)

7, *2(11.72)

배수 *2 -> *3

손연타

35.13~44.06(8.93)

34, *1(8.93)

배수 *1 -> *2

 

8000억개중의 베스트들이니 점수 변별력이 높지 않다. 부담도는 워낙 배수가 높아 변별은 되지만 사실 다들 거의 만점에 가까워 실제로는 별 의미가 없다. 분담률도 거의 다 만점 범위 안에서 약간씩만 이동하고 있어 잘 솎아 낸다. 만점을 낮게 잡아 약간씩 감점도 있는 편이어서 같은 기준 내에서는 가급적 좋은 배열을 찾아 낸다.

연타율은 골고루 퍼져 있는데 지연타가 낮아도 부담도가 높아서 감점이 많다. 1등 배열은 지연타가 11%나 되지만 부담도 때문에 좋은 점수를 받았다. 연타율과 부담도가 동시에 좋은 배열을 찾아야 하는데 드물다. 여기서 기준을 바꿔서 다시 돌려 봐야 뭔 의미가 싶기도 하고 dist5로 돌려 봐야 별반 다를 거 같지도 않다.

dist5는 현재 속도로 1년이나 걸린다. 그러나 dist5를 다 돌리지 않더라도 일부라도 돌려 보는 것도 의미는 있다. 중간 결과라도 과연 더 좋은 배열이 있는지 가능성은 알아 볼 수 있으니까, v, k를 고정하고 경우의 수를 줄여 +-5칸 움직이는게 더 좋은 배열을 뽑을 가능성도 배제할 수 없다. dist5 돌리는 와중에 어차피 스레드 노니 dist4도 같이 돌려도 된다. 기준은 다음과 같이 조정했다.

 

- 부담도의 만점을 15.4에서 15.38로 조금 더 내린다. 이러면 평균 감점이 10점 더 되는 셈인데 점수만 평행이동할 뿐 순위에는 변화가 없다. 어차피 부담도끼리는 같은 기준으로 상대 평가를 할 뿐이며 다른 기준과는 점수의 범위만 중요하다.

- 분담율과 연철은 현 상태를 유지한다. 대부분 범위안에 다 들어오며 벗어나도 많이 벗어나지는 않는다. 제곱 감점이어서 많이 벗어난 배열만 순위권에서 밀어내면 된다. 또 허용치가 높아 사실 그리 중요한 기준이 아니다.

- 연타 : 부담도에 비해 범위가 좁아 과소 평가되어 있다. 부담도는 20인데 손가락연타는 11이어서 높아도 별로 영향을 주지 않는다. 만점은 그대로 두고 애초에 설정했던 배수인 *3, *2를 원복한다.

 

이 외에 다른 요건이 더 있을 수도 있는데 이건 좀 더 돌려 보고 그때 생각해 봐야겠다. 일단은 수정한 기준으로 새로 뽑은 dist4 1위 배열을 잠정적인 영문 배열로 간주하고 다음 디자인을 계속하기로 한다. 프로그램도 약간 수정한다.

 

- 부담도와 감점의 정확도가 낮다. 둘 다 소수점 이하 4자리까지 정밀도를 높인다.

- 손연타와 손가락 연타는 리스트에도 소수점 이하 2자리까지 출력한다.

- 감점과 순위는 정수로 바꿔서 정렬한다. 문자열 정렬하니 순서대로 정렬되지 않아 불편하다. 아니면 아예 출력할 때 선행 제로를 붙이는 것도 괜찮을 것 같다.

 

이상의 수정 사항 적용하여 dist5 먼저 돌렸다. 이건 끝까지 해 볼 생각은 없고 하는데까지 해 볼 계획이다. 중간쯤에 스레드가 한가해지면 dist4도 같이 돌릴 생각인데 그것까지 고려하면 대략 2주는 더 돌려야 할 것 같다. 그동안 회사일에도 좀 신경쓰고 다른 공부도 좀 해야겠다. 여러 개 신경쓰다 보니 무척 피곤하다.

-------------------------------

저번주 수요일 쯤에 변경한 기준으로 dist4 채점이 끝났다. 더불어 하는데까지 해 보자고 시작한 dist5도 어느 정도 진척이 되었다. 어차피 중간에 끊을 예정이라 요가 노트북에도 같은 일을 시켜 두었다. 여기까지의 실행 결과는 다음과 같다.

 

dist4 : 6.7. 148/

dist5 : 8.87일째. 265/. 76조중 2조개 채점

        1: 3223~. 25.75

        1000: 3223~. 34.51

dist5(요가) : 5.79일째. 36조중 1조개 채점중. 204/

dist5(요가) 612일 아침 : 9.08일째. 57조중 1.6조개 채점중. 204/

 

확실히 4800H 노트북이 빠르긴 하지만 요가 노트북도 i7-8750H라 느리지는 않다. 스레드 개수가 적어도 노는 시간이 많아 그냥 이걸로 계속 돌리면 될 거 같다. 대략 300일 예상하는데 끝까지 돌리지는 못할 거 같고 하는데까지만 해 볼 계획이다. TUF 4800H는 작업을 끝내고 회사로 반입하여 주 개발 PC로 계속 쓸 예정이다.

변경된 기준으로 dist41등 배열은 다음과 같다. 예전의 배열과는 채점 기준이 달라져 정확히 비교하기는 어렵지만 달라진 것은 확실히 맞다.

 

tenoraslicuhdmwfpvkygbjxqz

부담도 15.4179. 18.95

좌우 51:49. 0

18:21:28:33-44:27:22:07 2.44

29:57:15. 0.28

지연타 : 6.97%. -0.10

손연타 : 35.13%. 2.26

연철 감점 : 0

감점 23.835

 

예전 1등에 비해 부담도는 약간 늘었지만 연타율이 대폭 감소했다. 이 정도면 채점 기준도 웬만큼은 정비가 된 것 같으니 당분간 이 배열을 기준으로 약간씩 조정해서 남은 작업을 하면 될 것 같다.

dist52조개를 채점한 결과는 1등이 etonaridsuchlpmwfkvygbjxqz이며 25.75점 감점이어서 dist41등보다는 아직 낮다. 1000등 점수가 34.51이어서 dist41등인 ten~ 배열이 이 범위안에 들지만 아직 여기까지는 순회조차 하지 않았다. t가 앞쪽으로 나오려면 몇 달 걸릴 거 같은데 하는데까지 해 보고 중간 중간에 비교해 보되 굳이 dist5까지 돌아야 하나 싶다.

여기까지 Ranking 프로젝트는 일단락한다. 마무리짓기 위해 소스 정리만 대충 다시 했다. 안쓰는 코드 지우고 함수 순서 조정 및 이름 변경하고 주석도 좀 달아 두었다. 채점 결과 읽은 후 재시작이 안되거나 다른 파일을 읽지 못하는 버그가 있어 수정했으며 채점 기준은 전혀 변경하지 않았다.

dist5는 참고가 될까 해서 돌리고 있고 좀 쉬었다가 다음 작업을 수행해야겠다. 아직 Enter의 위치도 제대로 결정하지 않았고 연습 프로그램도 만들어 보지 않았다. 최근에 회사일이 바빠져 당분간 본격적인 작업을 하기는 어려울 것 같고 지금 만든 배열로 특허 신청이나 해 둬야겠다.

21711: dist4로 재배치

6월은 노트북한테 채점만 시켜 놓고 회사일, SQL 정복 출판 및 추가 강좌 작성, 시스템 교체, 이클립스 정리 등의 작업을 진행했다. 이제 웬만큼 정리가 된 거 같으니 다시 키보드 작업을 시작한다. 2층의 요가 노트북은 혼자 외롭게 dist5를 채점하고 있는데 중간 결과는 다음과 같다.

 

5.9/269조 채점중. dist4의 전체 개수가 70조니 4배 정도 더 진행중

933시간(38)째 초당 175만개 채점중

1. 58465,2630,4323번째 temorasdiculhwm~~~

22.86점 감점. 부담도 15.41, 지연타 8%, 손연타 34%.

 

dist53500조중 40조개 정도로 예상했는데 10분의 1 정도 진행한 셈이다. 한달 넘게 돌리고는 있는데 계속 돌려야 하나 고민중이다. 윈도우의 강제 업데이트와 레노보의 바이오스 업데이트가 수시로 괴롭히고 있어 연속성을 보장받기도 어렵다. 중간 결과 저장 후 다른 노트북으로 갈아탈 수도 있지만 그러면 채점 시간 연속성이 없다. 이 부분은 미처 신경쓰지 못한 부분이다. 그냥 이대로 계속 가 보기로 하고 dist4의 최종 1등을 대상으로 배치해 보자.

배치를 다시 할려고 보니 그 전에 어떤 고민을 했는지 복습을 해야 한다. 39~ 15일 사이에 배치를 여러번 바꾸었고 그 후에는 채점에 치중했었다. 3월에 고민한 최종 배치와 채점에 의한 배열을 종합해 보면 현재는 다음 레이아웃이 최신이다.

이 레이아웃이 과연 이전의 고민을 다 반영한 것인지 자체 검증부터 거쳐야 한다. 그리고 채점 결과 적용한 영문 레이아웃이 과연 예전보다 효율적인지도 더 점검해볼 필요가 있다. 이 정도도 나쁘지 않은 배치이지만 다음은 더 고민해야 할 부분이다.

 

- Home, EndPgUp, PgDn을 현재처럼 수직으로 배치할지 수평으로 배치할지 결정. Home, End는 수평이 맞지만 PgUp, PgDn은 수직이 맞아 결정이 어렵다. 수평 배치가 더 좋지 않나 생각중이다. 이 부분은 다음과 같이 조정했다.

 

Home, End배치가 위치와 의미가 같아 상식적이고 PgUp도 손가락을 멀리 뻗을 필요가 없어 효율적이다. PgDn, Del이 쿼티와 위치가 같다는 점도 좋다.

- 오른 집안열에 잘 쓰지도 않는 Prev, Play, Next보다는 Cut, Copy, Paste를 넣는게 어떨까 싶다. 편집락을 고려하면 왼쪽 집안열도 괜찮고 오른쪽도 다른 편집키와 같이 있어 실용성 있을 거 같다.

- 한글 ㅋ 자리가 안 좋아 2행 왼집안에 놓는게 더 좋을 거 같은데 이 자리에 ㅕ가 옵션이지만 배치되어 있다. , , , ㅛ에 키를 할당할 것인가, 아니면 중복키를 빼고 ㅋ, ㅌ의 접근성을 높일 것인가를 결정해야 한다. 한 문자에 대해 두 가지 이상의 입력 방법 존재, 좌자우모의 원칙 등 여러 가지로 고려할 게 많다.

 

더 개선할 점은 없는지 점검도 해 보고 연습 프로그램에 적용해 봐야겠다. 이걸 터치 스크린에서도 구현해 보고 진짜 쓸만한지 검증해야 한다. 그리고 적당한 시점에 초안 완성한 후 특허도 내 놔야 1년 후에 제작을 시작해볼 수 있다. 아직 이름을 못 정했는데 그것도 고민이다.

----------------------------

713: 배치를 점검해 보려면 결국 Exercise 프로그램에 반영하여 직접 쳐 봐야 한다. 이 프로그램은 20127월 이후로 업데이트한 적이 없는데 이제 드디어 다시 만들 때가 되었다. 내가 만든거지만 분석하는데만도 상당한 시간이 걸릴 것 같다. 일단 분석해 보고 배치부터 만들어 보자. 다음 사항을 단계별로 수정했다.

 

- category 멤버를 기본, 쿼티 호환 0, 우편집, 좌매크로, 펑션키 다섯 개로 분류하고 레벨도 이 다섯개를 순서대로 표시하는 걸로 했다. 카테고리랑 레벨이 같아 멤버를 합쳐도 될 거 같은데 일단은 그대로 두었다.

- 키보드 배열 편집 편의를 위해 group을 앞쪽으로 옮기고 그룹 내의 좌표 xPOINT 구조체로 묶었다. 이 표를 편집하기 쉬워야 다양한 배치를 쉽게 만들어 볼 수 있다.

- 이모티콘 모드는 제거한다. Num 키를 더블 푸시하여 모드 전환한 후 3개의 페이지를 지원하는데 이렇게까지 할 필요는 없을 것 같다. 125월경에 모바일을 위해 넣은 기능이라고 되어 있는데 우연히 이모티콘 모드로 들어가 버리는 경우가 많고 소형 키보드가 이렇게 많은 문자를 지원할 필요가 없다.

- Num 모드는 Shift키와 함께 쓰지 않고 다 단독 문자이다. 문자 모드의 ,.=; 네 개키는 숫자 모드에서 쓸 수 없어 모드 바꿔야 한다. 다만 소수점 .은 숫자와 같이 많이 쓰므로 1 옆에 따로 중복해 두었다. 문제는 오른새밖열의 %, ", @인데 이 녀석들의 Shift 모드를 지원해야 하는가 하는 점이다.

지원하지 않으면 위쪽의 $, ', ^를 모드 바꾸고 Shift까지 눌러서 입력해야 하니 불편하다. 그래서 Num 모드에도 Shift는 필요하다. 이렇게 되면 왼쪽의 기호 영역 15개와 오른 집안열에도 Shift 자리가 남는데 여기에 이모티콘을 채워 넣을 수 있다. 오른 새끼열의 -, +, /도 마찬가지이다. Num 모드의 배치를 다음과 같이 수정했다.

여러 가지로 변화가 많다.

 

. 외에 ,도 숫자 구분자로 많이 사용하므로 숫자 모드에 중복 정의했다. ?!, =, _, ;, :은 어쩔 수 없이 모드 바꾼 후 Shift 누르고 입력해야 한다.

▲ 왼쪽 키에 이모티콘 15개를 배치했다. 임시로 많이 쓸 거 같은 키를 배치했는데 차후 조정 가능하다.

/*위로 이동했다. 연산자 4개가 숫자 좌우에 있어 직관적이기는 하다. 다만 /는 웹 주소나 경로 구분자로 많이 쓰는데 자리가 좀 안 좋아 보인다.

▲ 오른쪽 키에는 이모티콘을 배치하지 않아 Shift가 효력이 없다. 차후 더 많은 이모티콘을 배치하고 싶으면 이 영역을 쓸 수도 있다. 단 왼새밖열은 Shift에도 문자가 있어 안된다.

 

여기까지 작업해 놓고 테스트해 보니 Num 모드에서 Space를 눌러도 0이 입력되지 않는다. b1RowNum 변수에 따라 달라지며 툴바의 윗행수 버튼이 이 변수를 통제한다. 이게 도대체 뭔 옵션인지 당췌 기억이 나지 않아 기록을 뒤져 보니 12715일에 기호가 여기 저기 흩어져 있어 불편하며 1행에 숫자를 두고 기호를 한 곳에 모으기 위해 만든 옵션이다.

툴바를 누르면 b1RowNum 변수를 토글하고 Keyboard1, Keyboard2 두 배열 중 하나를 선택적으로 사용하는 구조인데 지금은 숫자를 Num 모드에 두거나 아니면 아예 58키 배치가 있어 필요 없다. 내가 만든 프로그램인데 히스토리가 가물가물하니 기록을 참조해야 한다. 관련 옵션을 죄다 없애 버리고 툴바에서도 제거했다.

이렇게 해 놓고 테스트해 보니 다 좋은데 엄지행의 Num, Edit, 한영 전환 키가 따로 없어 펑션키에 연결해 놓고 테스트해야 한다. 0행을 지금은 쓰지 않으니 아예 한칸씩 위로 올려 맵핑하면 괜찮을 거 같다. 사실 이 옵션은 전에도 있었는데 윗행수 옵션으로 대신 사용했던 거 같다.

툴바의 버튼은 살리고 키보드 배열에 vkup을 추가하여 맵핑을 한칸씩 올려서 테스트하도록 한다. 예전에 이미 구현했던 흔적이 있어 어렵지 않고 구현했다. 툴바도 살리고 엄지행을 알파벳키에 일단 맵핑해 두었다.

대부분의 맵핑이 잘 동작하는데 Shift+SpaceBS를 입력하는 기능은 윈도우 키보드의 한영 전환과 충돌이 발생하여 잘 동작하지 않는다. 그리고 한칸 위로 올리니 쿼티와 들여쓰기가 달라 은근 불편하다. 이 모드는 그냥 엄지행을 위한 테스트 기능 정도밖에는 안될 거 같다. 디폴트를 false로 두고 필요할 때만 켜 보기로 한다. 제대로 테스트하려면 역시 물리 키보드를 만들거나 아니면 터치 키보드라도 있어야겠다.

윗층에 올라고 yoga 노트북 임무를 종료했다. 재부팅하겠다고 화면 중앙에 떡 하니 떠 있다. 업데이트 서비스 중지시키고 레노보 업데이트도 끄고 오만 지랄을 해도 강제 재부팅을 막을 방법이 없다. 진짜 이런 강제 업데이트 정책은 혐오스럽다. 오늘까지 작업한 양은 다음과 같다. 대략 일주일 정도 더 돌렸지만 1등은 바뀌지 않았다.

 

68750억개 채점/308. 1079시간 동안 초당 176만개 채점

1등은 58465~. 22.8675

 

요가를 갖고 내려와 멀티 터치를 테스트해 봤는데 대충 되기는 하지만 홀드 다운이 안된다. 마우스 다운으로는 안되고 별도의 제스처 메시지를 처리해야 하는 모양인데 이 부분은 아직 연구해 보지 않았다. 좀 더 연구해 본 후 배치 테스트를 해야겠다.

다음은 랭킹도 좀 더 업데이트해야겠다. 컴퓨터를 바꿔 가며 돌려 볼 수 있다는 것은 분명 잘 만들어 놓은 것이기는 한데 시간과 개수가 연속되지 않는다. 그러다 보니 이후부터 돌리는 것은 속도 계산이 부정확하고 전체 개수도 알 수 없다.

이 정보도 저장해야 돌리다가 일 생기면 잠시 중지해 놓고 다시 돌릴 수 있다. 다음 사항을 수정했다. 다행히 작업한지 얼마 되지 않아 코드를 보는데 큰 어려움은 없었다.

 

- 버전 번호 추가 및 버전 확인

- 총개수, 경과 시간 저장 후 복구

- 경과 시간은 일단위로 표시

- 개수는 조 단위까지 표시

- 완료 후 버튼 캡션 "종료"로 변경

- 채점 개수 외에 순회 개수도 표시. 만개 단위로

- 파일명의 시간에 선행제로 붙여 저장순으로 정렬되도록 함

- 이전 버전과 구분하기 위해 파일 확장자 .ranking으로 수정

 

현재 속도라면 1년 정도 걸리는데 도저히 이 정도까지 기다릴 수가 없다. 좀 더 최적화를 하거나 빠른 컴퓨터를 구해 3개월안에 끝낼 수 있도록 개선을 좀 해 봐야겠다. dist4 결과를 배열에 적용한 것도 좋은데 이왕이면 특허를 dist5까지 구한 후 내는게 좋을 거 같다.

21722: 랭킹 최적화

랭킹 프로그램 최적화를 단행했다. 최대한 불필요한 사항 제거한 후 메인의 순회 루프를 스레드가 덜어주도록 기획했다.

 

- Sampling 옵션 제거. 초기에 중간 중간 찍어 보기 위해 만든건데 지금은 필요치 않다. 오히려 조건 판단하느라 시간만 까 먹는다. 컨트롤과 옵션 모두 제거했다.

- 배치 크기 실행중 조정 기능 제거 : 실행중에 변경 가능하다 보니 동기화가 필요한데 이것도 시간을 잡아 먹는다. 대략 100개 정도면 더 조정할 필요 없이 충분하다. 채점 시작 전에만 조정할 수 있도록 수정했다. 이 두 작업을 한후 실행 시간은 113초에서 112초로 1초 줄었다. 거의 효과가 없었다.

- 저장 파일명에 RankingKeyboard 이름을 빼고 날짜로만 적었다. 어차피 확장자로 구분 가능하니 파일명을 너무 길게 쓸 필요 없다.

- layout_id 제거하고 layout_num으로 번호만 유지. 최초 이 변수는 적합 배열이 몇 개인지 세 볼려고 한 건데 dist에 따라 달라지는 값이라 별 의미가 없다. 총 개수는 그냥 22!11해이다. 샘플링 주기 결정, 초당 채점수 계산에 사용하는데 둘 다 layout_num으로 대신할 수 있다. 메인과 스레드가 적합 여부를 나누어 평가하려면 이 변수를 동기화해야 하는데 그게 어려워 제거했다.

layout_num으로 총 개수만 세기로 하되 이것도 NextLayout에서 1차로 거른 후의 일련 번호여서 배열의 고유 ID로는 쓸 수 없다. 배열은 그냥 알파벳 22자로 구분하는 것이 정확하다. 베스트 출력 주기도 억단위로 바꾸고 파일 포맷에 id를 제외하여 버전도 101로 높였다. 이 작업 후 14초로 약간 개선 효과가 있었다.

- 메인에서 0~21자리까지 살펴 보고 적합성을 점검하는데 이 점검을 뒤쪽 일부만 하고 나머지는 스레드에게 맡기도록 했다. 22글자를 다 보면 시간이 걸리니 메인은 최대한 빨리 순회하고 스레드가 적합성 점검을 떠 맡으면 순회가 빨라질 거 같다. 어차피 스레드 남아 도니 메인만 빨라지면 CPU도 풀 활용할 수 있다. 점검을 전부 다 넘기면 메인이 배열을 복사하는 속도가 느리니 일부만 점검하기로 했다.

 

18~21까지만 메인이 점검 : 315

15~21까지만 메인이 점검 : 28

10~21까지만 메인이 점검 : 121

 

결론적으로 이 작전은 처음 기대와는 달리 완전히 실패다. 메인이 적합성 점검에 시간을 덜 쓰는 것은 맞지만 배열을 일단 복사해서 스레드로 넘겨야 하는데 이 시간이 더 오래 걸린다. 작업을 분할하는게 더 비효율적이다. 그래서 원복시켰다. 잘 될거 같았는데 막상해 보면 오히려 역효과가 나는 것도 있게 마련이다.

- 적합성 점검 분할 작전이 실패했으니 layout_id도 괜히 없앤 거 같다. 이게 과연 필요한지 다시 생각해 봤는데 dist 조건에 맞아 실제 채점한 개수일 뿐 사실 별다른 의미는 없다. 순회 개수의 대략 100분의 1 정도라는건 이미 알고 있으니 이왕 없애 버린 거 그냥 없애 버리기로 한다. layout_num이 총 개수가 아니라 적합 배열만을 대상으로 하므로 베스트 출력 주기도 1/10로 다시 조정했다.

- NextLayout 최적화. 처음 계획은 메인에서 전체 자리를 다 보는 것보다 순회하는 NextLayout에서 적합성을 점검하면 최소 범위만을 점검할 수 있지 않을까 하는 것이었다. reverse하는 범위가 주로 뒤쪽이니 그 앞쪽은 이미 적합해 점검할 필요가 없다. 또 스왑후 i 위치가 부적합이면 리턴하지 말고 바로 재시도하면 된다. i가 적합하면 reverse한 이후만 점검했는데 예상치 못한 문제가 있었다. dist2조건에서 다음과 같은 경우이다.

 

1.15 17 18 16 21 20 19 - i=16자리 j=19자리

2.15 17 18 19 21 20 16 - 스왑후 i자리의 19는 범위안이다.

3.15 17 18 19 16 20 21 - reverse16이 세 칸 벗어나 again으로 점프

                     - i20, j21. 이 둘을 스왑

4.15 17 18 19 16 21 20 - reverse한 뒤쪽 20, 21은 범위안이 맞음. 16이 범위밖이다.

 

상황이 좀 복잡한데 16이 제일 뒤로 갔다가 reverse한 자리가 범위 밖으로 이미 부적합한데 다음 순회에서 2120만 교환하므로 16reverse 대상이 아니며 점검 범위 밖이 되어 버린다. 뒤쪽만 점검하니 부족합 배열을 걸러내지 못한다.

이 상황을 방지하려면 3번 상황일 때 즉, reverse후 역탐색하여 범위밖을 찾았을 때 그 뒤쪽은 순회하지 않도록 내림차순 정렬해 버리면 된다. i 번째 자리가 범위 밖으면 그 오른쪽은 어떻게 배열해도 범위 밖으므로 아예 범위의 마지막 상태인 내림차순 정렬해 버린다.

3번 상태에서 again으로 가기 전에 4번 상태로 만들어 버리는 것이다. 그러면 16 21 20이 되고 다음번 순회시는 i 번째인 16은 다시 교환 대상이므로 정상적인 흐름을 계속 유지할 수 있다. i가 한참 앞쪽이면 강제 내림차순 정렬해 버려 중간의 수많은 배열을 아예 건너뛰어 버릴 수 있다.

NextLayout 알고리즘은 오름차순을 점점 내림차순으로 변경하며 순회한다. 스왑 후 i자리가 부적합일 때는 reverse를 생략하여 오름차순으로 만들지 않는데 i 뒤쪽은 이미 내림차순이다. reversei자리가 부족합이면 내림차순 정렬하여 끝 상태로 만들어 버린다. 이 알고리즘에서 내림차순은 순회 완료를 의미한다.

이 현상을 수학적으로 증명하기는 어렵고 사실 확신이 서지도 않지만 dist2로 돌려 보면 이전 버전과 결과, 개수가 정확히 똑같다. 디버거로 직접 돌려 보지 않으면 이해하기 어렵다. 나도 다음에 보면 분명 이해하기 어려울 것이다. 처음부터 순회하여 51번째에 3번 상황이 되며 여기서부터 단계 실행해 보면 현상을 이해할 수 있다. 여기까지 작업한 후 다시 속도를 측정해 봤다.

 

dist2 : 29초만에 완료. 5676만개. 초당 195만개

dist3 : 1시간. 133억개. 초당 373만개.

 

dist3Enum으로 세는데 14시간, 채점하는데 8시간 36분 걸렸던 것을 이만큼 개선했다. 노트북 교체와 그간의 최적화를 감안해도 최소 다섯 배는 빨라졌다. 직전 버전의 dist2 기준으로 해도 2.5배의 향상이다. NextLayout에서 적합성을 완벽히 걸러내어 리턴하므로 main의 적합성 점검 루프는 제거했다. 메인의 루프가 빠지면 더 빨라질 거 같았는데 여전히 29초이다. 이건 사실 잘 이해가 안가는데 5600만개 배열을 일일일 재점검하는 속도가 1초도 안 걸렸다니 의아스럽다. 코드에 뭔가 헛점이 있나 싶어 좀 더 점검해 봐야겠다.

------- 724

새로 최적화한 코드로 메인 노트북(4800h)에 출근한 동안 일을 시켜 놓고 갔다. 12시간 돌려본 결과는 다음과 같다.

 

4800h : 1800억개 채점중. 12시간. 417/

요가 : 6910억개/22. 318시간. 210/초 채점, 6750/초 순회

 

노트북 속도가 다르지만 4일 걸릴 작업을 이틀이면 해 치울 정도이니 확실히 더 빨라지기는 했다. 이 정도 속도면 dist540조개를 채점하는데 111일이 걸린다는 계산이 나온다. 4개월이면 되니 해 볼만 하다.

사람 욕심이라는게 참 끝도 없다. 더 빨라질 수 있지 않을까 싶어 코드를 여기 저기 뜯어 보고 극한의 최적화를 시도해 보게 된다. 현재 NextLayout의 소스는 다음과 같다.

 

inline int compare(const void* a, const void* b)

{

     // 같은 숫자인 경우는 절대 없음.

     // if (*(int *)a == *(int *)b) return 0;

     if (*(int *)a > *(int *)b) return -1;

     return 1;

}

// 레이아웃 순회.

inline bool NextLayout()

{

     int i, j;

     int left, right;

 

again:

 

     // 왼쪽이 오른쪽보다 작은 최초의 위치 찾기

     for (i = LAST - 1; i >= 0; i--) {

          if (arLayoutInt[i] < arLayoutInt[i + 1]) break;

     }

 

     // 다 내림차순이면 순열의 끝임

     if (i == -1) return false;

 

     // i 위치값보다 더 큰 최초의 위치 찾기

     for (j = LAST; arLayoutInt[j] <= arLayoutInt[i]; j--) { ; }

 

     // i, j의 값 교환

     SWAP(arLayoutInt[i], arLayoutInt[j], t);

 

     // i위치로 교환한 문자가 범위안일 때만 reverse 실행, 아니면 건너뜀

     if (ABS(arLayoutInt[i] - i) <= Dist) {

          // 뒷부분 오름차순으로 정렬

          for (left = i + 1, right = LAST; left < right; left++, right--) {

              SWAP(arLayoutInt[left], arLayoutInt[right], t);

          }

     } else {

          goto again;

     }

 

     // reverse한 뒷부분만 적합성을 점검한다.

     for (int idx = LAST; idx >= i + 1; idx--) {

          if (ABS(arLayoutInt[idx] - idx) > Dist) {

              // 부적합이 발견된 뒷부분을 내림차순 정렬하여 중간 상태를 건너뛰어 버린다.

              // 뒤에 남은 수가 0, 1일 때는 정렬할 필요 없고 2이면 크기 판별 후 SWAP 한다.

              // 2 이상은 qsort로 내림차순 정렬한다.

              switch (idx) {

              case LAST:

              case LAST - 1:

                   break;

              case LAST - 2:

                   if (arLayoutInt[idx + 1] < arLayoutInt[idx + 2]) SWAP(arLayoutInt[idx + 1], arLayoutInt[idx + 2], t);

                   break;

              default:

                   qsort(&arLayoutInt[idx + 1], LAST - idx, sizeof(int), compare);

                   break;

              }

              goto again;

          }

     }

 

     // dist 조건에 맞는 배열만 리턴. 번호는 순회할 때마다 증가한다.

     layout_num++;

     return true;

}

 

reverse 후 뒤쪽 남은게 0, 1개인 경우는 정렬이 필요 없다. 2개인 경우는 단순한 조건문으로 판단 후 직접 SWAP 하고 그 이상인 경우만 qsort 함수로 내림차순 정렬한다. 같은 경우는 절대 없으니 비교 함수에서 같은 경우는 주석 처리해서 판단하지 않는다. 이런 식으로 단 한 클럭이라도 아끼려고 온갖 노력을 다했다.

여기서 qsort 함수의 효율성에 대해 의심을 품고 좀 더 빠른 방법을 찾아 보았다. 회사에서 검색해 보니 이것보다 C++sort가 더 빠르다고 한다. 과연 그런지 테스트 예제를 만들어 보았다.

 

int compare(const void* a, const void* b)

{

     if (*(int *)a == *(int *)b) return 0;

     if (*(int*)a > *(int*)b) return 1;

     return -1;

}

 

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)

{

     HDC hdc;

     PAINTSTRUCT ps;

     int ar[] = { 5, 6, 10, 2, 20, 1, 12, 16, 3, 19, 4, 8, 18, 11, 17, 7, 13, 9, 15, 14};

     int ar2[20];

     DWORD st, ed;

     TCHAR str[128];

 

     switch (iMessage) {

     case WM_CREATE:

          hWndMain = hWnd;

          return 0;

     case WM_PAINT:

          hdc = BeginPaint(hWnd, &ps);

          EndPaint(hWnd, &ps);

          return 0;

     case WM_LBUTTONDOWN:

          st = GetTickCount();

          for (int i = 0; i < 100000000; i++) {

              memcpy(ar2, ar, sizeof(ar));

              sort(&ar2[0], &ar2[sizeof(ar) / sizeof(ar[0])]);

          }

          ed = GetTickCount();

          wsprintf(str, TEXT("%d"), ed - st);

          SetWindowText(hWnd, str);

          return 0;

     case WM_RBUTTONDOWN:

          st = GetTickCount();

          for (int i = 0; i < 100000000; i++) {

              memcpy(ar2, ar, sizeof(ar));

              qsort(&ar2[0], sizeof(ar)/sizeof(ar[0]), sizeof(int), compare);

          }

          ed = GetTickCount();

          wsprintf(str, TEXT("%d"), ed - st);

          SetWindowText(hWnd, str);

          return 0;

     case WM_DESTROY:

          PostQuitMessage(0);

          return 0;

     }

     return(DefWindowProc(hWnd, iMessage, wParam, lParam));

}

 

공정한 비교를 위해 20개의 무작위 난수 배열을 생성하고 매번 이걸 복사해서 다시 정렬한다. 기정렬 상태에 따라 알고리즘 실행 속도에 차이가 있고 16개 미만도 실행 방식이 달라지기 때문이다.

qsort는 개수를 지정하는데 비해 sort는 끝 위치를 지정하고 끝은 범위에서 제외됨을 유의해야 한다. 20개를 1억번 복사 및 정렬한 후 경과 시간을 측정했다.

 

qsort : 47.3

sort : 4.6

 

거의 10배 정도 차이가 난다. memcpy 호출이 있어 순수한 정렬 속도만은 아니지만 대충봐도 차이가 많음을 알 수 있다. 배열 크기가 바뀌면 달라질 수도 있어 크기를 5로 줄인 후 다시 측정해 봤다.

 

qsort : 5.75

sort : 0.81

 

어쨌거나 sort가 더 빠르다. qsortC 함수고 sortC++ 함수인데 예상과 달리 C 함수가 더 느리다. 그 이유는 다음과 같다.

 

- qsort는 비교를 위해 compare를 호출하여 함수 호출 오버헤드가 있다. sortless 또는 greater 함수 객체로 비교하는데 이 코드가 인라인 처리되어 더 빠르다.

- qsort는 임의의 대상을 정렬하기 위해 비교 함수로 void * 주소를 넘긴다. 비교 함수는 이걸 다시 대상의 타입으로 캐스팅한 후 비교하니 느리다. sort는 템플릿 기반이어서 타입이 이미 정해져 있으며 정수는 값 자체를 바로 비교할 수 있다.

 

퀵 소트가 빠른 알고리즘인 건 맞지만 요소의 배열 상태나 크기 등에 영향을 받아 최악의 경우는 효율이 떨어진다. sort는 삽입, , 퀵 세 가지 알고리즘을 개수와 상황에 따라 조합해서 적용한 introsort 알고리즘을 사용한다. 16개 이하면 삽입 정렬을 쓰고 그 이상이면 퀵이나 힙을 쓰도록 되어 있어 빠르다.

reverse에서 부적합 발견시 정렬하는 코드를 다음과 같이 수정했다. 뒤쪽 남은게 2개인 경우 단순 교환하는 코드만 남겨 두고 sort 함수로 바꾸었다.

 

     // reverse한 뒷부분만 적합성을 점검한다.

     for (int idx = LAST; idx >= i + 1; idx--) {

          if (ABS(arLayoutInt[idx] - idx) > Dist) {

              // 부적합이 발견된 뒷부분을 내림차순 정렬하여 중간 상태를 건너뛰어 버린다.

              // 뒤에 남은 수가 0, 1일 때는 정렬할 필요 없고 2이상이면 SWAP 한다.

              if (idx <= LAST - 2) {

                   if (idx == LAST - 2) {

                        if (arLayoutInt[idx + 1] < arLayoutInt[idx + 2])

                             SWAP(arLayoutInt[idx + 1], arLayoutInt[idx + 2], t);

                   } else {

                        sort(&arLayoutInt[idx + 1], &arLayoutInt[LAST + 1], greater<>());

                   }

              }

              goto again;

          }

     }

 

테스트 예제에서는 분명히 효과가 있었는데 기대와는 달리 28초로 고작 1초 정도 속도 개선 효과가 나타났다. CPU는 거의 모든 스레드를 100% 다 사용한다.

25초 정도만 빨라져도 좋을텐데 효과가 미미해 좀 아쉽다. 이제 거의 극한까지 다 최적화를 한 것인가 싶기는 한데 조금 더 방법을 찾아 보기 위해 직접 삽입 정렬을 시도했다. 혼연C에 정리해 놓은 코드를 그대로 가져와 내림차순으로 부등호만 바꿔서 코드 작성했다.

 

                        // idx + 1 ~ LAST까지 삽입 정렬 직접 실행

                        int ii, jj;

                        for (ii = idx + 2; ii <= LAST; ii++) {

                            for (t = arLayoutInt[ii], jj = ii; jj > idx + 1; jj--) {

                                 if (arLayoutInt[jj - 1] < t) {

                                      arLayoutInt[jj] = arLayoutInt[jj - 1];

                                  } else {

                                      break;

                                 }

                            }

                             arLayoutInt[jj] = t;

                        }

 

훌륭하게 잘 동작하고 결과도 정확하게 나오지만 실행시간 단축 효과는 없었다. sort16개 이하는 삽입 정렬로 실행되므로 결국 같은 코드인 셈이다. 똑같이 28초가 걸리니 그냥 sort를 쓰기로 한다.

기계가 바뀌거나 강제 업데이트 등으로 채점을 중단해야 하는 경우가 종종 있어 파일 저장 기능을 만들었고 버전도 기록한다. 그런데 테스트해 보니 버전을 기록할 때, 읽을 때 일치시키지 않아 제대로 점검이 안되었다. VER 상수에 버전 관리하고 이 버전으로 기록 및 읽도록 했다. 이전 버전의 데이터는 이제 읽을 수 없고 꼭 필요하다면 컨버팅 루틴을 만들어야 한다. 현재 버전은 101이다.

1시간 정도 돌리던 파일을 다시 읽어와 돌려 봤는데 이어서 채점하기가 잘 되는 거 같은데 결과 출력 주기가 좀 맞지 않다. 1000, 1, 10억 단위로 주기를 조정했더니 대략 6분 후에 결과가 갱신되었다. 주기를 어떻게 조정해도 좀 불편한 면이 있다. 갈수록 느려지는 것보다는 개수만 자주 갱신하고 베스트 목록은 천천히 갱신하기로 했다.

 

- PrintFreq는 천만으로 고정한다. 초당 400만개이면 대략 2.5초에 한번꼴이다. 이 값을 조정하는 코드는 모두 뺐다.

- PrintBestbTimeOnly 인수를 전달하고 파일 읽은 후, 일시 정지시는 false를 전달하고 WM_USER + 1에서는 true를 전달하여 시간만 갱신한다.

- 시간만 갱신해도 100번에 한번꼴로 베스트를 출력한다. 10억번에 한번꼴이며 250초에 한번이니 주기가 심하지는 않다.

- 첫 실행시에는 천만을 기다려야 돌아가는게 보이는 불편함이 있어 lastPrintNum을 음수인 -PrintFreq * 0.99로 초기화하여 최초 10만개를 세면 바로 보이도록 했다.

- WM_USER + 1에서는 시간만 갱신하지만 베스트 목록이 비었을 때, 즉 최초 1번은 베스트 목록을 보인다. 10만개를 센 후이므로 1000개는 이미 목록에 있다.

 

그리고 데이터 파일의 생성 위치도 현재 디렉토리에서 실행 파일이 있는 위치로 변경했다. 개발 환경에서는 자꾸 프로젝트 폴더를 찾아 데이터 파일이 여기 저기 흩어져 불편하다. 실행 파일이랑 같은 폴더에 있는게 제일 찾기 쉽고 비교하기도 좋다.

여기까지 작성한 후 요가랑 터프랑 똑같이 실행해 보니 요가는 초당 228만개, 터프는 초당 431만개를 채점한다. 메인이 빨라지니 스레드 많은게 확실히 효과를 톡톡히 보고 있다. 최소 16스레드 이상의 시스템을 구해 돌리면 대략 3개월 안에 dist5는 끝낼 수 있는 셈이다.

호기심에 NextLayout이 건너뛰는 정도가 얼마나 되나 싶어 swap시와 reverse시에 건너뛰는 횟수를 카운트해 보았다. 실제 채점 속도에 영향을 미치지 않도록 디버그 모드에서만 카운팅하도록 했다. dist2를 디버그 모드에서 채점해 본 결과 총 시간은 19초 걸리며 결과는 다음과 같다.

 

again_swap : 65066665

again_reverse : 208312572

 

이렇게나 건너뛰니 11해중에 5676만개만 골라 채점할 수 있는 것이다. 건너뜀에 의해 중간 생략되는 것은 더 엄청나다. 11해는 11억억이니 비율상 0.5/11억이고 대략 20억개중 하나만 채점하는 셈이다. 진짜 최적화의 위력이 엄청나다.

최적화는 이 정도면 충분한 거 같고 일단락짓기 위해 코드를 리팩토링했다. 하면서 다음과 같은 자잘한 사항을 개선했다.

 

- 작업스레드에서 통계 리셋할 때 최대치인 MAXBATCHNUM이 아닌 BatchNum 개수만 리셋. 실행중에 변경하지 않으므로 최대치만큼 리셋할 필요 없다.

- PassOverCount 변수 제거. 연타 점검을 건너뛴 횟수를 세는데 채점과는 무관한 정보여서 제거했다.

- 디버그 모드에서만 쓰는 관찰용 변수 전부 조건부 컴파일 블록 안으로 집어 넣음

- MainSleep, EvSleep, AggSleep 변수 제거. 예전에 슬립을 옵션화할 때 쓰던건데 지금은 필요 없다. 어차피 뒤에 가면 메인만 바쁘고 작업 스레드는 적당히 논다.

- 전역 hThread[MAXTHREAD] 배열 제거. 쓰지도 않고 arThread로 대체되었음.

- WinMain 함수에 주석 불일치 경고 발생. 인수 앞에 이상한 매크로를 붙여 주면 경고 사라진다. 정확한 의미는 연구해 보면 알겠지만 별로 알고 싶지 않다.

int APIENTRY WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance

     , _In_ LPSTR lpszCmdParam, _In_ int nCmdShow)

- 경고 제거. 주로 NULL 관련 문제인데 if문으로 체크해 주되 너무 한다 싶은건 경고 꺼 버림. 예를 들어 mallocNULL 리턴할 수도 있다, 그걸 대입받은 변수가 NULL일 수도 있다 등인데 그럴 가능성 없다.

 

     hRSrc = FindResource(g_hInst, MAKEINTRESOURCE(IDR_TEXT2), "TEXT");

     if (hRSrc != NULL) {

          Size = SizeofResource(g_hInst, hRSrc);

          hMem = LoadResource(g_hInst, hRSrc);

          if (hMem != NULL) {

              ptr = LockResource(hMem);

              str = (char*)malloc(Size + 1);

              memcpy(str, ptr, Size);

              str[Size] = 0;

              SetDlgItemText(hWnd, EDSAMPLE, str);

          }

     }

 

- strcat(str, max); 코드에 대해 maxNULL 종료 아닐 수도 있다고 경고 발생. char max[111] = { 0, };로 선언문에서 초기화해 주니 해결됨.

- 정렬 범위 컨트롤 폭 좀 키우고 저장, 읽기, Reset 버튼도 폭 90으로 늘림.

- 파일명이 너무 길어 간략하게 조정하고 버전 번호 앞쪽에 명시했다. 1분에 두 번 채점할 일은 없을 거 같아 초 단위 생략하고 101-210724-2031.keyrank 식으로 정리했다. 시간 사이에 : 넣어 놓고 왜 저장안되지? 하고 순간 헷갈렸다.

 

여기까지 코드 정리해 놓고 다시 실행해서 원래 결과와 맞는지 비교해 봤다. 시간은 27초로 약간 단축되었지만 상황에 따라 차이가 날 수 있어 별 의미는 없다. 이것 저것 최적화 많이 했는데 결과가 똑같이 나와야 한다. dist2는 캡처 떠 놓은 것과 똑같이 나오는 거 확인했고 dist4를 돌리고 시작했다.

이 데이터는 이후에도 재사용 가치가 있어 이전 버전과 똑같은지 정확히 확인해야 한다. 이 점검이 중요한 이유는 지금까지 한 최적화가 과연 논리상 문제가 없는지, 저번 버전과 같은지 확인하기 위해서이다. 이후 채점 방식을 변경하더라도 여기까지는 문제가 없음을 분명히 확인한 후 바꿔야 한다. 예를 들어 jxqz의 위치를 변경하면 채점 결과가 달라지는데 지금 바꾸면 정합성을 확인할 수 없다.

이전 버전으로는 8600억개에 6.7일 걸렸는데 최적화에 의해 2~3일이면 되지 않을까 기대한다. 요가 노트북에서 돌리면 아마도 더 오래 걸릴 것 같은데 예전과 달리 스레드 개수에 영향을 받기 때문이다. 얼마나 느린지 같이 돌리고 있다. 혹시나 해서 x64로 컴파일해서 실행해 봤는데 실행 파일만 약간 더 크고 속도는 거의 비슷하다. dist4 실행중 속도를 비교해 봤다.

 

tuf : 2480, 527/. 13:3

yoga : 1480, 319/. 12:51

 

CPU 많은 tuf가 역시 빠르다. , 회사에서 각각 하나씩 쓰고 있는데 하나 더 구해야겠다. 어제부터 중고 장터 째려 보고 있는데 좀체 매물이 나오지 않다고 오늘 은평구에 하나 나와 사러 가기로 했다. 요거 사서 통계나 시켜 먹어야겠다.

자다가 문득 순회랑 메인이랑 스레드를 분리할까 생각해 봤다. 스레드 여유가 있으니 메인을 둘로 쪼개면 이론상은 더 빨라진다. 그러나 초당 500만개나 되는 순회 결과를 전달할 마땅한 방법이 없고 그 메모리 복사하는 시간이 더 오래 걸릴 거라 하나마나일 거 같아 포기했다.

최적화는 이제 이쯤 했으면 충분히 했다. 0724버전을 최종으로 하고 더 시간 쓰느니 노트북한테 빨리 빨리 일이나 시키는게 더 나을 거 같다. 최적화하는 내 인건비도 생각해야지. dist5 예상 4개월이면 해 볼만한거다. 이거 목록 뽑은 후 다음에는 베스트1000의 각 배열을 조금씩 변형해 보는 방식을 기획중이다.

현재 순회 알고리즘은 키위치별 순위에 기반해서 앞뒤로 어느 정도 움직여 보는 식이다. 예를 들어 2번 자리는 앞쪽을 0, 1까지 뒤쪽으로 3, 4, 5, 6, 7까지 움직여 보며 점수를 내 본다. 이런 선형적인 이동은 변형폭이 많지 않고 평면적인 키보드 배치와는 잘 맞지 않은 면이 있다. 2번을 아래 위, 대각선까지 움직인다면 11, 12, 9, 13, 16으로도 바꿔볼만 하다.

모든 배열에 대해 이 짓을 일일이 해 보기는 부담스럽다. 그래서 1차 예선을 통과한 베스트 1000에 대해서만 정밀하게 재변형을 가해 보자는 작전이다. 1000개에 대한 변형은 개당 1000개씩 해 본다고 해도 고작 10만개에 불과하니 순간적이다. 이런 목적이라면 베스트를 1000개에서 3000개 정도로 늘려야 하나도 좀 생각해 봐야겠다.

위 그림에서 윗행과 아랫행의 순위는 윗행 우선이다. 아래로 접는 3행보다는 위로 뻗는 1행이 더 쉽다고 판단한 것이다. 그런데 집게 안쪽열은 좀 다른 거 같다. 22번과 24번의 순서가 애매한데 이 순위에 따라 jxqz의 위치가 바뀐다. 이걸 바꾸면 채점 기준이 달라지는데 일단 1행 우선으로 잡고 돌린 후 최종적으로 두 위치를 다시 한번 고려해 봐야겠다.

------------------------

725

본격적인 dist5를 채점하기 전에 몇 가지 개선 사항을 더 적용했다. 4개월을 돌리려면 준비가 좀 더 치밀해야 할 거 같아 하루 더 투자한다.

 

- 베스트 개수를 조정한다. 개수가 늘어나면 스레드가 집계하는 시간이 걸리니 아무래도 시간이 더 걸린다. BESTNUM만 늘리면 될 줄 알았는데 바로 다운되어 버려 점검해 보니 늘어난만큼 BestBuf도 같이 늘려 주어야 한다. 10배 늘려 주었다. 개수별 채점 시간은 다음과 같다.

 

1000- 27, 3000- 28, 5000- 31, 10000- 34

 

선형적으로 늘어나는데 이건 집계보다는 리스트뷰에 출력하는 시간의 문제인 것 같다. 그래서 베스트는 100개만 출력하고 에디트에에는 출력하지 않으며 정지, 완료시에만 전체 출력하도록 했다. 이렇게 했더니 약간 빨라지긴 하는데 10000개는 32, 5000개는 30초이다.

그런데 dist2는 최초 한번만 출력하기 때문에 결국 최초 한번 출력이 그만큼의 시간을 잡아 먹었다는 얘기다. 5000개는 좀 많은 거 같고 부담스럽지만 후반부로 갈수록 베스트 중간에 끼어드는 경우가 드물 거 같아 과감하게 5000개를 채택하기로 한다. 베스트는 정렬하지 않고 교체하는 방식이고 스레드가 베스트를 순차검색으로 찾기 때문에 초반에 좀 느리다가 삽입 횟수가 줄어들면 빨라진다.

- 에디트에 출력하는 문자열에 순위인 i가 빠져 선두에 i를 집어 넣었다. 텍스트 형식은 차후 결과를 빼낼 때 유리할 거 같아 조립하는데 이것도 시간이 걸리니 중간에는 출력하지 않도록 해 두었다.

- 에디트에 출력하는 정보는 거의 참고 정보인데 이걸 더 위에 출력하는게 맞지 않는 거 같다. 리스트뷰와 에디트의 위치를 바꾸어 에디트를 바닥에 깔아 두었다. 이건 실행중에 볼 일이 사실 별로 없다. 높이 100으로 했다가 너무 불쌍해진 거 같아 200으로 살짝 높여 주었다.

- 실행 파일 경로에 파일을 만들기 위해 다음 코드로 경로에서 실행 파일명을 제거하는 코드를 사용했다. 경로 다음의 실행 파일명만 지우는 것이다.

 

Current[lstrlen(Current) - lstrlen("RankingKeyboardLayout.exe")] = 0;

 

이랬더니 파일명 다음에 =0724 등의 버전을 붙이면 Ranki101-210725-2142 따위로 잘리다 마는 현상이 일어났다. 실행 파일명이 항상 일정할거라는 가정이 틀린 것이다. strrchr \를 찾은 후 이 문자를 지워 경로를 추출했다.

- 베스트 개수가 늘어 났으니 버전도 올려야 한다. 채점 기준은 바뀌지 않았지만 파일의 용량이 달라지는거니 올리는게 맞다. 102로 버전을 올렸다.

 

여기까지 작업한 버전을 725일자 버전 102로 정의하고 당분간 이걸로 dist5를 채점하기로 한다. 자잘한 버그와 수정 사항 일일이 고치다가는 더 진척이 안되니 큰 버그 없으면 이걸로 가자. dist4도 샘플 개수가 바뀌었으니 천천히 다시 채점할 것이다. 채점하는동안 제스처 연구하고 배치 재조정한 후 샘플 조정하기로 한다.

dist5 채점을 위해 새로운 노트북을 서울 은평구까지 가 하나 더 장만했다. 어제 자면서 메인 노트북을 돌려 봤는데 시끄럽고 계속성도 보장되지 않았다. 요가는 너무 느리고 결국 tuf4800h를 통계용으로 하나 더 들였다. 이거 세팅한 후 바로 dist5 채점에 투입했다. 지금 옷장 밑에서 외롭게 채점을 시작했는데 대략 4개월 예상한다.

-------------------

726

최적화는 그만 할려고 했는데 문득 또 다른 개선 방안이 생각났다. 어제 베스트 개수에 따라 속도가 달라지는 것을 보고 원래 1000개도 적지 않았던 거 같다. 지금 이 루틴이 순차 검색을 사용하는데 처음부터 정렬을 하고 이분 검색을 하면 속도가 좀 더 개선될 거 같다. 속도보다도 베스트 크기를 키워도 많이 영향을 받지 않을 거 같아 만 개까지 늘릴 수 있다.

회사에서 알고리즘 좀 생각하다가 집에 와서 코딩해 보기로 했다. 크게 기대하지는 않지만 5% 정도만 빨라져도 좋고 속도는 비슷하더라도 CPU 점유율이 떨어져 발열에도 유리하지 않을까 생각된다. 아직 해 보지 않았는데 지금부터 딱 1시간 30분을 더 투자해서 열코딩해 볼 것이다. 작성한 코드는 다음과 같다.

 

          EnterCriticalSection(&critBest);

          for (eidx = 0; arThread[tidx].arEvaluate[eidx].layout_num != -1; eidx++) {

              sEvaluate* pEv = &arThread[tidx].arEvaluate[eidx];

              // 배열 마지막의 최대 감점보다 크거나 같으면 삽입할 필요 없다.

              if (pEv->MinusTotal >= arBest[NUMBEST - 1].MinusTotal) {

                   continue;

              }

 

              // 이분 검색으로 삽입할 지점을 찾는다. 같은 값을 찾는게 아니어서 bsearch는 사용할 수 없다.

              lower = 0;

              upper = NUMBEST - 1;

              for (;;) {

                   mid = (lower + upper) / 2;

                   // 값을 찾았으면 그 자리에 삽입한다. 실수값이라 이럴 경우는 드물다.

                   if (arBest[mid].MinusTotal == pEv->MinusTotal) {

                        iidx = mid;

                        break;

                   }

 

                   // 대소 비교하여 절반씩 범위를 좁힌다.

                   if (arBest[mid].MinusTotal > pEv->MinusTotal) {

                        upper = mid - 1;

                   } else {

                        lower = mid + 1;

                   }

 

                   // 두 범위가 만나면 lower와 대소 비교해서 위치를 결정한다. lower가 더 크면 이 자리에 삽입하고 더 작으면 lower 아래에 삽입한다.

                   if (upper <= lower) {

                        if (arBest[lower].MinusTotal > pEv->MinusTotal) {

                             iidx = lower;

                        } else {

                            iidx = lower + 1;

                        }

                        break;

                   }

              }

 

              // iidx 이후를 전부 한칸 아래로 내려서 복사한다.

              memcpy(&arBest[iidx + 1], &arBest[iidx], sizeof(sEvaluate) * (NUMBEST - iidx - 1));

 

              // iidx 자리에 pEv를 덮어쓴다.

              arBest[iidx] = *pEv;

          }

          LeaveCriticalSection(&critBest);

 

최대 감점과 1차 비교 후 베스트에 끼지 못하면 그냥 스킵한다. 범위 안이면 삽입할 지점을 찾는데 이분 검색이 부등 비교여서 좀 복잡하고 확실하게 맞는지 확신도 서지 않는데 결과는 비슷하게 나온다. 이전에 캡처 떠 놓은 것에 비해 id가 하나씩 작은데 이건 이전 버전의 다음 코드 때문이다.

 

if (bOk) {

     layout_id++;

     // 현재 순열 전달.

 

main에서 적합성을 통과하면 id를 증가시켜 스레드로 전달하며 그 후에 NextLayout을 호출한다. 그래서 id1베이스였다. 이에 비해 현재 버전은 일단 전달하고 NextLayout에서 다음 배열을 찾아 적합성을 점검한 후 id를 증가시키므로 0베이스이다. 꼭 어떤게 맞다고 단정할 수는 없는데 현재의 0베이스를 택하기로 하고 다음번 버전업할 일이 있으면 그때 1부터 시작하도록 수정해 보기로 한다. 아무튼 이 문제는 논리상의 문제는 아니므로 넘어가기로 한다.

코드를 작성한 후 dist2를 테스트해 보니 45초가 걸려 시간이 오히려 더 늘었다. 다 짜 놓고 생각해 보니 순차 검색은 하지 않지만 새로운 베스트가 나올 때마다 memcpy를 대량으로 하고 있어 그렇다. 매 배치마다 5000번 루프를 돌며 max를 찾는 시간과 베스트 찾을 때마다 memcpy 하는 시간차로 인해 오히려 더 느려진 것이다. 베스트 크기가 늘어나면 메모리 이동량도 늘어나 더 심해질 거 같다. 애초의 기대와는 달리 베스트 크기는 5000개 이상 늘리기 어렵다.

그런데 이 문제는 베스트 확률이 점점 줄어드는 후반부로 갈수록 단점이 상쇄되는 효과가 있다. 매번 무조건 순회하는 것에 비해 배치 100개에 대해 continue만 하는건 확실히 이득이다. 과연 그런지 dist4로 장시간 점검을 해 보았다. 결과는 다음과 같다.

 

2:  순차:3.2900271/, 이분:2.9089236/

13: 순차:35.7979 458/, 이분:34.9554447/

25: 순차:73.1280486/ , 이분:71.1199473/

 

초반에는 순차 검색이 빠르다가 중반에 이분 검색이 거의 따라 잡는다. 그러나 뒤로 가면 둘 다 빨라지다가 순차 검색이 더 빨라진다. 더 뒤로 가면 어떨지 모르겠지만 순차 검색도 베스트 확률이 떨어지면 최대 감점을 찾는 횟수가 줄어 들어 속도 향상 효과가 분명히 있다. 결론적으로 이번 작전은 실패이다. 어제자로 소스 원복해 놓고 계속 쓰기로 한다.

-------------------------

727

아침에 요가 노트북에 시켜 놓은 dist4 채점이 끝났다. 그동안 수행한 최적화가 이전 버전에 비해 문제가 없는지 점검하기 위해서이다. 먼저 이전 버전인 530일자의 dist4의 결과를 보자. 이때는 15.6일이 걸렸었다.

다음은 최적화한 후 다시 채점해 본 것이다. 이번에는 2일하고 10시간 걸렸는데 중간에 요가로 바꾼 것까지 고려하면 최소 5배 정도는 더 빨리진 것 같다.

결과는 똑같다. 끝 등수인 999까지 비교해 봐도 역시 똑같이 나온다. 그러나 아쉽게 개수가 하나 차이가 나는데 이건 0베이스로 바뀌어서 그렇다. 이것도 결국 수정해야겠다.

이분검색 실패 후 회사에서 곰곰히 생각해 봤는데 너무 16K되는 큰 배열을 5000개씩이나 밀기를 초당 500만번에서 베스트에 든 거 대략 10만개만 쳐도 엄청난 양의 메모리를 밀고 당겨서 그렇다. 배열은 그대로 두고 인덱스만 따로 만들어 관리하면 검색도 빠르고 삽입도 빠른 양쪽의 이득을 다 취할 수 있을 것 같다. 데이터베이스의 넌클러스터 인덱스를 그대로 따라해 보는 것이다. 오늘도 대략 2시간 정도 이 작업을 해 볼 계획이다.

 

- 시작 번호를 1base로 수정한다. SetDlgItemText(hWnd, STLAYOUT, "1(etaonishrdlumcwygfpbvkjxqz)") 로 수정하면 이 숫자로부터 시작하므로 1부터 시작된다. 이전 버전과 레이아웃 번호를 일치시켰다.

- MinusTotal 멤버가 결국 최종 점수인데 철자가 길어 Score로 이름을 바꾸었다. 최종 점수이기도 하니 적절한 이름이다.  있던 Scorereserved[100]은 버전 관리 기능이 들어갔으니 제거한다.

- 인덱스를 저장할 다음 구조체를 선언한다. arBest에 대한 순서값을 가지는 배열이다. 최초 실행시 0~NUMBEST까지 순서대로 번호를 가지며 점수는 arBest의 초기 점수인 1000점으로 초기화한다.

 

struct sBestIndex

{

     double Score;

     __int64 bestIdx;

};

sBestIndex arIndex[NUMBEST];

 

- 이제 최고 감점은 arBest[arIndex[NUMBEST - 1].bestIdx].Score로 한단계를 더 거쳐야 구할 수 있다. 이 값보다 더 감점이 적을 때만 병합한다. 이분검색으로 인덱스상의 삽입 위치를 찾아 둔다. 인덱스 마지막의 최고감점 자리를 현재 배열로 덮어 쓰고 iidx 이후를 memcpy로 아래로 민다. 최고 득점은 밀려나 사라진다. iidx에는 새로 덮여쓴 베스트의 첨자와 현재 배열의 점수를 기록해 둔다.

 

arBest         arIndex

0 12 80.5          68.5 3

1 23 94.2         72.0 2

2 34 72.0         80.5 0

3 56 68.5         94.2 1

 

이 상태에서 현재 배열이 num 7777.7점이라고 하자. arIndex의 끝 최고감점인 94.2보다 적으니 병합된다. 94.2가 원래 있던 arBest1번 자리에 이 배열을 덮어쓴다. 삽입할 위치는 80.5자리의 iidx 2번이 되며 2번 이후는 한칸씩 아래로 민다. 94.2는 자연히 사라진다. 2번 자리에 새로 삽입한 77.7 점수와 이 배열이 새로 기록된 arBest의 첨자 1번을 기록한다.

 

arBest         arIndex

0 12 80.5          68.5 3

1 77 77.7         72.0 2

2 34 72.0         77.7 1

3 56 68.5         80.5 0

 

arBest는 가장 고감점 자리를 덮어쓰고 arIndex는 제일 아래쪽의 최고감점을 밀어내고 정렬상의 위치에 삽입하여 새 점수와 새로 차지한 arBest의 첨자를 기록해 둔다. arBest는 레코드만 교체하고 arIndex는 삽입하여 정렬 상태를 유지하는 것이다.

- best를 출력할 때는 arBest를 직접 참고하지 않고 arIndex를 순서대로 읽으며 bestIdx 번째 배열을 읽으면 된다. 인덱스가 이미 정렬되어 있으므로 따로 정렬할 필요는 없다. 이후 pEv의 멤버를 읽어 리스트뷰와 에디트에 뿌리면 순서대로 출력된다.

 

     for (int i = 0; i < numBest; i++) {

          sEvaluate* pEv = &arBest[arIndex[i].bestIdx];

 

- 파일로 저장할 때도 정렬 순서에 따라 저장한다. 꼭 정렬상태로 저장할 필요는 없지만 arBest를 정렬하지 않으면 arIndex도 같이 저장해야 하는 부담이 생긴다.

 

     for (int i = 0; i < Count(arBest); i++) {

          sEvaluate* pEv = &arBest[arIndex[i].bestIdx];

          WriteFile(hFile, pEv, sizeof(sEvaluate), &dwWritten, NULL);

     }

 

- 다시 읽어올 때는 레코드를 순서대로 읽고 인덱스는 순서에 맞게 다시 만들어 준다. 정렬되어 있으니 인덱스는 0번부터 끝까지 번호 매기고 점수만 기록해 두면 된다.

 

     for (int i = 0; i < Count(arBest); i++) {

          ReadFile(hFile, &arBest[i], sizeof(sEvaluate), &dwRead, NULL);

          arIndex[i].bestIdx = i;

          arIndex[i].Score = arBest[i].Score;

     }

 

- 병합하기 전에 병합이 과연 필요한지 점검한다. 배치내의 최저감점을 찾아 놓고 이 점수가 베스트의 최고감점보다 더 높으면 병합할 필요 없다.

 

              nowScore = EvaluateLayout(&arThread[tidx].arEvaluate[eidx]);

              if (nowScore < minScore) {

                   minScore = nowScore;

              }

....

          if (minScore < arBest[arIndex[NUMBEST - 1].bestIdx].Score) {

              // arBest 동시 접근 방지

              EnterCriticalSection(&critBest);

 

불필요하게 크리티컬 섹션으로 들어가지 않음으로써 스레드간의 경쟁을 방지하는 효과가 있다. arBest가 동기화 객체여서 한 스레드가 액세스중이면 다른 스레드가 대기해야 하고 결국 메인도 대기하기 때문에 접근 횟수를 줄여야 한다.

- MaxMinusInBest 변수 제거. 최고감점을 별도의 변수에 관리해 두고 천만개 이후 중간 감점이 이 점수 이상이면 연철 체크를 생략하는 역할을 했다. 이제 인덱스를 통해 최고 감점을 언제든 찾을 수 있어 불필요하다. 이 변수가 없어지면 순회하지 않고 버튼 눌러 특정 배열을 점검할 때 문제가 좀 있을 수 있는데 layout_num이 천만 이상일 때만 점검하도록 되어 있어 순회하지 않고 바로 채점하면 된다.

- 채점을 시작할 레이아웃을 지정하는 스태틱을 에디트로 변경했다. 이는 직접 ID를 지정함으로써 두 컴퓨터에 나누어 분할 채점을 해 보기 위해서이다. 최초 시작할 때 중간 배열을 적어 두면 두 컴퓨터가 반씩 나누어 작업할 수 있다.

중간이 어디쯤인지는 dist4를 돌려 보고 num이 중간쯤인 배열을 사용하면 될 것 같은데 배열은 알 수 있어도 num을 알기는 어렵다. 분할 채점했을 경우 병합이 또 문제인데 이건 다음에 생각하자.

 

여기까지 작업하는데 딱 2시간 정도 걸렸다. 알고리즘을 뜯어 고치는 복잡한 작업이지만 회사에서 충분히 기획을 해 왔고 별다른 논리적 어려움이 없어 비교적 짧은 시간에 무사히 작업을 마쳤다. 역시 C 코딩 실력은 아직 녹슬지 않았다.

측정해 보니 1000, 5000개일 때는 26초로 약간 단축되었고 10000개이면 29초로 시간이 약간 늘어나기는 한다. 최적화도 했으니 그냥 10000개로 가기로 한다. 초반부는 베스트가 많아 정확한 성능 비교가 어려워 dist4로 조금 장시간 돌려 보았다.

 

2 :  순차:3.2900 271/, 이분:2.9089 236/-> 3.5291

13 : 순차:35.7979 458/, 이분:34.9554 447/-> 39.0500

25 : 순차:73.1280 486/ , 이분:71.1199 473/-> 81.1 541

 

베스트를 만개로 늘렸는데도 불구하고 속도는 더 좋아졌다. 역시 인덱스는 효과가 있다. 게다가 개수가 늘어나면 속도가 점점 빨라지는데 이것도 일정 개수 이상에서는 한계가 있을 거 같다. CPU 점유율은 골고루 잘 써 먹고 있으며 대략 40~80 사이를 왔다 갔다 한다. 속도가 빨라져도 알고리즘이 좋아져 혹사시킬 것 같지는 않다.

베스트 개수가 늘었고 채점 구조체에 여분 멤버도 제거했지만 파일 버전은 계속 102로 유지한다. 왜냐하면 102 버전으로 저장한 파일이 아직 없기 때문이다. dist4로 비교하여 논리상의 문제점은 점검했으니 이전까지 저장한 건 다 필요 없고 102부터 새로 시작한다.

여기까지 작업한 내역을 727일자로 명명하고 이걸로 다시 측정을 시작한다. 11시 정각에 요가에게는 dist4를 맡기고 tuf에게는 dist5를 맡겼다. tuf는 새로 사온 후 123시간동안 초당 496만개로 8464억개를 채점하고 있던 중이었는데 관두고 새로 임무를 부여했다. 요가는 dist4 채점 후 터치 연구용으로 쓸 예정이다.

---------

81

그제 저녁에 요가에게 시켜둔 dist4 채점 작업이 완료되었다. 223시간 6분 걸렸으니 거의 사흘 가까이 걸린 셈이며 초당 330만개 정도 채점했다. TUF가 초당 600만개가 넘는데 비해 절반 속도밖에 안나는 셈인데 CPU 클럭 차이도 있지만 스레드 개수의 차이가 큰 것 같다. 결과는 다음과 같다.

다음은 68일 완료한 dist4 채점 결과이다. 당시에는 TUF로 초당 148만개씩 6.7일이 걸렸으니 서너배는 더 빨라진 셈이다. 배열 개수는 8615억개로 정확히 일치하고 1~10위까지 배열을 비교해 보면 완전히 같다. 최적화에 의한 채점 논리의 파괴가 없었음을 명확히 증명했다.