Microsoft MVP성태의 닷넷 이야기
.NET Framework: 295. 괜찮은 문자열 해시 함수? [링크 복사], [링크+제목 복사],
조회: 36403
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)

괜찮은 문자열 해시 함수?

오늘 재미있는 글을 하나 읽었습니다. ^^

컴파일 중에 문자열 해시 만들기....(를 시도해 보자? -_-)
; http://www.gamedevforever.com/50

위의 이야기 중에 보면, x65599 hash 함수 하나를 소개하고 있는데요. 소스 코드가 아래와 같이 실려져 있습니다.

// 65599를 곱하는 해시함수. (Red Dragon 책에서 훔쳐옴 -0-)
unsigned int generateHash(const char *string, size_t len)
{
  unsigned int hash = 0;
  for(size_t i = 0; i < len; ++i)
  {
     hash = 65599 * hash + string[i];
  }
  return hash ^ (hash >> 16);
}

마침, 우리 회사에서도 사용하고 있던 해시 함수가 있었는데 위의 함수에 비하면 무거운 연산을 하기 때문에 전체 메일로 소개를 했더랬지요. ^^

그러자, 자체 hash 충돌 코드로 테스트한 결과가 피드백으로 날아왔습니다.

테스트 문자열 경우의 수: 14,776,336

x65599 hash  => 해시 충돌=722,750
사내 해시함수 => 해시 충돌=0

충돌 횟수가 비교될 정도로 많습니다. 여기서 갑자기 오기가 발동한 성태... ^^ 곧바로 x65599 해시 함수 개량에 나섰습니다.

(*** 이후의 테스트 결과들은 확실한 비교를 위해 문자열 경우의 수를 기존 14,776,336에서 162,539,696으로 늘렸습니다.)

자... 이제 차근히 x65599 hash 함수를 보았습니다. 우선... 함수의 마지막 return 문에서 수행한 hash 쉬프트는 그다지 영양가가 없다는 판단이 들었습니다. 실제로 shift 연산을 제거하고 테스트했더니 오히려 충돌 횟수가 줄어드는 결과값이 나왔습니다.

x65599 hash
걸린 시간: 61,374 ms, collisions = 35,374,354 (21%)

shift 제거한 hash
걸린 시간: 50,091 ms, collisions = 20,820,228 (12%)

충돌 수도 줄고, 속도도 빨라졌으니 x65599 hash는 일단 다음과 같이 바뀌는 것이 좋겠습니다.

int hash = 0;
int len = chars.Length;
for (int i = 0; i < len; ++i)
{
    hash = 65599 * hash + (int)chars[i];
}

return hash;

약간 나아지긴 했지만, 여전히 충돌 횟수가 걸립니다. 좀 더 개선할 수는 없을까요? 가만 보니까 사용된 소수(Prime number)값 - 65599가 마음에 안 듭니다. 구하려는 해시 값 자체가 전체 4byte인데 겨우 2 바이트 정도에 해당하는 65,599 값만을 쓴다는 것이 왠지 충돌의 원인이 아닐까 싶어서 CRC32 알고리즘에서 사용되는 값의 하나인 0xEDB88320 수로 대체를 해보았습니다. 다행히, 테스트 결과 ... 충돌 횟수가 어느 정도는 줄었습니다.

0xEDB88320 hash
걸린 시간: 53,705 ms, collisions = 2,633,400 (1.62%)

근데... 과연 0xEDB88320 값이 올바른 선택일까요? 짝수라는 것이 왠지 좀 그래서, 또 다른 CRC32 값 중에서 홀수인 0x741B8CD7 값과 혹시나 싶어서 그 값보다 큰 수 중에서 소수를 찾아 (0x741b8cf1) 다시 한번 테스트를 돌려보았습니다.

또 다른 CRC32 0x741B8CD7 hash
걸린 시간: 55,080 ms, collisions = 131,040 (0.08%)

소수 0x741b8cf1 hash
걸린 시간: 58,075 ms, collisions = 3,926,600  (2.42%)

오호... 결과를 보니, 소수라고 해서 항상 정답은 아닌 것 같습니다. ^^ 그러고 보면, CRC-32에서 사용되는 값이 괜히 채택된 것은 아닌 것 같습니다.

개인적으로 여기서 만족할 수 없더군요. 좀 더 값을 분산시켜 볼 수 있지 않을까 싶어서 bit 연산을 생각했습니다. 위에서 테스트 했던 16진수 값을 대상으로 각각 테스트를 해보았는데요. 결과는 의외로 0xEDB88320의 승리였습니다.

0xEDB88320 + 1bit Left Shift
걸린 시간: 56,407 ms, collisions = 4,500 (0.0028%)

0x741B8CD7 + 1bit Left Shift
걸린 시간: 54,360 ms, collisions = 240,065 (0.148%)

0x741b8cf1 + 1bit Left Shift
걸린 시간: 54,906 ms, collisions = 381,888 (0.235%)

여기까지 하고... 다시 회사에서 사용하던 CRC32 알고리즘과 충돌 횟수를 비교해 보았습니다.

사내 해시함수
걸린 시간: 135,649 ms, collisions = 1,265,312 (0.778%)

오호~~~ 압도적으로 "0xEDB88320 + 1bit Shift" 연산이 승리를 했군요. 그래서 최종적으로 제가 만든 해시 코드는 다음과 같습니다.

static int hash(char[] chars)
{
    int hash = 0;
    int len = chars.Length;

    unchecked
    {
        uint poly = 0xEDB88320;
        for (int i = 0; i < len; i++)
        {
            poly = (poly << 1) | (poly >> (32 - 1)); // 1bit Left Shift
            hash = (int)(poly * hash + chars[i]);
        }
    }

    return hash;
}




참고로, 테스트를 위한 문자열 생성에 사용된 함수는 다음과 같습니다.

TestHashLib.TestHash hashTable2 = new TestHashLib.TestHash();
x = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

char[] xx = x.ToCharArray();
int tot = 0, dup = 0;
for (int c1 = 0; c1 < x.Length; c1++)
{
    for (int c2 = 0; c2 < x.Length; c2++)
    {
        for (int c3 = 0; c3 < x.Length; c3++)
        {
            for (int c4 = 0; c4 < x.Length; c4++)
            {
                for (int c5 = 0; c5 < x.Length; c5++)
                {
                    if (c5 > 10)
                    {
                        break;
                    }
                    char[] c = new char[] { xx[c1], xx[c2], xx[c3], xx[c4], xx[c5] };
                    int hash = hashFunction(c);
                    tot++;
                    if (hashTable2.containsKey(hash))
                    {
                        dup++;
                    }
                    else
                    {
                        hashTable2.put(hash);
                    }
                    c = null;
                }
            }
        }
    }
}

Console.WriteLine("Total: " + tot + ", time: " + st.ElapsedMilliseconds + ", collisions = " + dup);

위에서 보면, 마지막 c5 변수의 값을 10 이하로 제한했는데요. 왜냐하면, 메모리가 너무 많이 소비되어서 그런 조치를 취한 것입니다. 10으로만 해도 테스트를 위해 5GB 가까운 메모리를 소비하기 때문에 어쩔 수 없이 제약을 두었습니다.

메모리 소비로 인해, 당연히 x86 환경에서는 테스트 할 수 없고 Hash 값을 보관하기 위한 자료구조도 C#의 것이 아닌 C++/CLI를 통해 CAtlMap을 가져다 써야만 했습니다.

.NET 64비트 응용 프로그램에서 왜 (2GB) OutOfMemoryException 예외가 발생할까?
; https://www.sysnet.pe.kr/2/0/946

여러분들도 테스트를 직접 해볼 수 있도록, 위의 것들을 테스트 하는 데 사용한 코드를 첨부했습니다.

마지막으로, 위와 같이 결과가 나왔다고 해서 제가 공개한 해시 함수가 제일 좋을 것이다라는 판단을 하시면 안됩니다.

Dictionary.Get(A) 대신 Dictionary.Get(A.GetHashCode())를 사용해서는 안되는 이유
; https://www.sysnet.pe.kr/2/0/889

해시 함수는, 결국 해당 '업무 도메인'에서 사용되는 문자열 셋이 다르기 때문에 (가능하다면) 그에 따른 적절한 테스트를 해보고 선택하시는 것이 좋습니다.





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/9/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2012-01-20 12시26분
괜찮은 문자열 해시 함수? - 두 번째 이야기
; http://www.sysnet.pe.kr/2/0/1223
정성태
2015-08-18 07시27분
[백곰] static int hash(char[] chars) {
    int hash = 0;
    int len = chars.Length;
    unchecked {
        uint poly = 0xEDB88320;
        for (int i = 0; i < len; i++) {
            poly = (poly << 1) | (poly >> (32 - 1)); // 1bit Left Shift
            hash = (int)(poly * hash + chars[i]);
        }
    }

    return hash;
}
코드를 보면 초기 hash값이 0이기 때문에 처음에는 poly값을 활용하지 못하고 chars[0]값이 그대로 들어오게 되어 있는데 의도인가요?
[guest]
2015-08-18 01시01분
글쎄요. 저도 알 수 없군요. 그 부분의 정확한 답은 x65599 원 저작자에게 물어봐야 할 것 같습니다. 단지 추정을 해보자면, 의도라고 봐도 좋을 것 같습니다. 한 글자 내에서는 정확히 hash가 충돌 없이 나오므로 굳이 첫 글자에 poly 값을 적용할 필요는 없을 테니까요. ^^
정성태
2017-08-08 08시55분
[cok2529] 잘 보고 갑니다 !
[guest]

... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12322정성태9/11/202021821개발 환경 구성: 513. Azure VM의 RDP 접속 위치 제한 [1]
12321정성태9/11/202016918오류 유형: 648. netsh http add urlacl - Error: 183 Cannot create a file when that file already exists.
12320정성태9/11/202019492개발 환경 구성: 512. RDP(원격 데스크톱) 접속 시 비밀 번호를 한 번 더 입력해야 하는 경우
12319정성태9/10/202018401오류 유형: 647. smigdeploy.exe를 Windows Server 2016에서 실행할 때 .NET Framework 미설치 오류 발생
12318정성태9/9/202017172오류 유형: 646. OpenVPN - "TAP-Windows Adapter V9" 어댑터의 "Network cable unplugged" 현상
12317정성태9/9/202021068개발 환경 구성: 511. Beats용 Kibana 기본 대시 보드 구성 방법
12316정성태9/8/202018696디버깅 기술: 170. WinDbg Preview 버전부터 닷넷 코어 3.0 이후의 메모리 덤프에 대해 sos.dll 자동 로드
12315정성태9/7/202021204개발 환경 구성: 510. Logstash - FileBeat을 이용한 IIS 로그 처리 [2]
12314정성태9/7/202021373오류 유형: 645. IIS HTTPERR - Timer_MinBytesPerSecond, Timer_ConnectionIdle 로그
12313정성태9/6/202020942개발 환경 구성: 509. Logstash - 사용자 정의 grok 패턴 추가를 이용한 IIS 로그 처리
12312정성태9/5/202027978개발 환경 구성: 508. Logstash 기본 사용법 [2]
12311정성태9/4/202020481.NET Framework: 937. C# - 간단하게 만들어 보는 리눅스의 nc(netcat), json_pp 프로그램 [1]
12310정성태9/3/202019750오류 유형: 644. Windows could not start the Elasticsearch 7.9.0 (elasticsearch-service-x64) service on Local Computer.
12309정성태9/3/202018406개발 환경 구성: 507. Elasticsearch 6.6부터 기본 추가된 한글 형태소 분석기 노리(nori) 사용법
12308정성태9/2/202020875개발 환경 구성: 506. Windows - 단일 머신에서 단일 바이너리로 여러 개의 ElasticSearch 노드를 실행하는 방법
12307정성태9/2/202021806오류 유형: 643. curl - json_parse_exception / Invalid UTF-8 start byte
12306정성태9/1/202018411오류 유형: 642. SQL Server 시작 오류 - error code 10013
12305정성태9/1/202020814Windows: 172. "Administered port exclusions"이 아닌 포트 범위 항목을 삭제하는 방법
12304정성태8/31/202019240개발 환경 구성: 505. 윈도우 - (네트워크 어댑터의 우선순위로 인한) 열거되는 IP 주소 순서를 조정하는 방법
12303정성태8/30/202019613개발 환경 구성: 504. ETW - 닷넷 프레임워크 기반의 응용 프로그램을 위한 명령행 도구 etrace 소개
12302정성태8/30/202019760.NET Framework: 936. C# - ETW 관련 Win32 API 사용 예제 코드 (5) - Private Logger파일 다운로드1
12301정성태8/30/202018714오류 유형: 641. error MSB4044: The "Fody.WeavingTask" task was not given a value for the required parameter "IntermediateDir".
12300정성태8/29/202019618.NET Framework: 935. C# - ETW 관련 Win32 API 사용 예제 코드 (4) CLR ETW Consumer파일 다운로드1
12299정성태8/27/202020103.NET Framework: 934. C# - ETW 관련 Win32 API 사용 예제 코드 (3) ETW Consumer 구현파일 다운로드1
12298정성태8/27/202019945오류 유형: 640. livekd - Could not resolve symbols for ntoskrnl.exe: MmPfnDatabase
12297정성태8/25/202019398개발 환경 구성: 503. SHA256 테스트 인증서 생성 방법
... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...