/* ********************************************************************** * Copyright (C) 2010-2014, International Business Machines * Corporation and others. All Rights Reserved. ********************************************************************** * file name: dicttrieperf.cpp * encoding: US-ASCII * tab size: 8 (not used) * indentation:4 * * created on: 2010dec09 * created by: Markus W. Scherer * * Performance test program for dictionary-type tries. * * Usage from within <ICU build tree>/test/perf/dicttrieperf/ : * (Linux) * make * export LD_LIBRARY_PATH=../../../lib:../../../stubdata:../../../tools/ctestfw * ./dicttrieperf --sourcedir <ICU build tree>/data/out/tmp --passes 3 --iterations 1000 * or * ./dicttrieperf -f <ICU source tree>/source/data/brkitr/thaidict.txt --passes 3 --iterations 250 */ #include <stdio.h> #include <stdlib.h> #include "unicode/bytestrie.h" #include "unicode/bytestriebuilder.h" #include "unicode/localpointer.h" #include "unicode/ucharstrie.h" #include "unicode/ucharstriebuilder.h" #include "unicode/uperf.h" #include "unicode/utext.h" #include "charstr.h" #include "package.h" #include "toolutil.h" #include "ucbuf.h" // struct ULine #include "uoptions.h" #include "uvectr32.h" #include "cmemory.h" // for UPRV_LENGTHOF // Test object. class DictionaryTriePerfTest : public UPerfTest { public: DictionaryTriePerfTest(int32_t argc, const char *argv[], UErrorCode &status) : UPerfTest(argc, argv, NULL, 0, "", status), numTextLines(0) { if(hasFile()) { getLines(status); for(int32_t i=0; i<numLines; ++i) { // Skip comment lines (start with a character below 'A'). if(lines[i].name[0]>=0x41) { ++numTextLines; // Remove trailing CR LF. int32_t len=lines[i].len; UChar c; while(len>0 && ((c=lines[i].name[len-1])==0xa || c==0xd)) { --len; } lines[i].len=len; } } } } virtual UPerfFunction *runIndexedTest(int32_t index, UBool exec, const char *&name, char *par=NULL); const char *getSourceDir() const { return sourceDir; } UBool hasFile() const { return ucharBuf!=NULL; } const ULine *getCachedLines() const { return lines; } int32_t getNumLines() const { return numLines; } int32_t numTextLines; // excluding comment lines }; // Performance test function object. // Loads icudt46l.dat (or whatever its current versioned filename) // from the -s or --sourcedir path. class PackageLookup : public UPerfFunction { protected: PackageLookup(const DictionaryTriePerfTest &perf) { IcuToolErrorCode errorCode("PackageLookup()"); CharString filename(perf.getSourceDir(), errorCode); int32_t filenameLength=filename.length(); if(filenameLength>0 && filename[filenameLength-1]!=U_FILE_SEP_CHAR && filename[filenameLength-1]!=U_FILE_ALT_SEP_CHAR) { filename.append(U_FILE_SEP_CHAR, errorCode); } filename.append(U_ICUDATA_NAME, errorCode); filename.append(".dat", errorCode); pkg.readPackage(filename.data()); } public: virtual ~PackageLookup() {} // virtual void call(UErrorCode* pErrorCode) { ... } virtual long getOperationsPerIteration() { return pkg.getItemCount(); } // virtual long getEventsPerIteration(); protected: Package pkg; }; struct TOCEntry { int32_t nameOffset, dataOffset; }; // Similar to ICU 4.6 offsetTOCLookupFn() (in ucmndata.c). static int32_t simpleBinarySearch(const char *s, const char *names, const TOCEntry *toc, int32_t count) { int32_t start=0; int32_t limit=count; int32_t lastNumber=limit; for(;;) { int32_t number=(start+limit)/2; if(lastNumber==number) { // have we moved? return -1; // not found } lastNumber=number; int32_t cmp=strcmp(s, names+toc[number].nameOffset); if(cmp<0) { limit=number; } else if(cmp>0) { start=number; } else { // found s return number; } } } class BinarySearchPackageLookup : public PackageLookup { public: BinarySearchPackageLookup(const DictionaryTriePerfTest &perf) : PackageLookup(perf) { IcuToolErrorCode errorCode("BinarySearchPackageLookup()"); int32_t count=pkg.getItemCount(); toc=new TOCEntry[count]; for(int32_t i=0; i<count; ++i) { toc[i].nameOffset=itemNames.length(); toc[i].dataOffset=i; // arbitrary value, see toc comment below // The Package class removes the "icudt46l/" prefix. // We restore that here for a fair performance test. const char *name=pkg.getItem(i)->name; itemNames.append("icudt46l/", errorCode); itemNames.append(name, strlen(name)+1, errorCode); } printf("size of item names: %6ld\n", (long)itemNames.length()); printf("size of TOC: %6ld\n", (long)(count*8)); printf("total index size: %6ld\n", (long)(itemNames.length()+count*8)); } virtual ~BinarySearchPackageLookup() { delete[] toc; } virtual void call(UErrorCode * /*pErrorCode*/) { int32_t count=pkg.getItemCount(); const char *itemNameChars=itemNames.data(); const char *name=itemNameChars; for(int32_t i=0; i<count; ++i) { if(simpleBinarySearch(name, itemNameChars, toc, count)<0) { fprintf(stderr, "item not found: %s\n", name); } name=strchr(name, 0)+1; } } protected: CharString itemNames; // toc imitates a .dat file's array of UDataOffsetTOCEntry // with nameOffset and dataOffset. // We don't need the dataOffsets, but we want to imitate the real // memory density, to measure equivalent CPU cache usage. TOCEntry *toc; }; #ifndef MIN #define MIN(a,b) (((a)<(b)) ? (a) : (b)) #endif // Compare strings where we know the shared prefix length, // and advance the prefix length as we find that the strings share even more characters. static int32_t strcmpAfterPrefix(const char *s1, const char *s2, int32_t &prefixLength) { int32_t pl=prefixLength; s1+=pl; s2+=pl; int32_t cmp=0; for(;;) { int32_t c1=(uint8_t)*s1++; int32_t c2=(uint8_t)*s2++; cmp=c1-c2; if(cmp!=0 || c1==0) { // different or done break; } ++pl; // increment shared same-prefix length } prefixLength=pl; return cmp; } static int32_t prefixBinarySearch(const char *s, const char *names, const TOCEntry *toc, int32_t count) { if(count==0) { return -1; } int32_t start=0; int32_t limit=count; // Remember the shared prefix between s, start and limit, // and don't compare that shared prefix again. // The shared prefix should get longer as we narrow the [start, limit[ range. int32_t startPrefixLength=0; int32_t limitPrefixLength=0; // Prime the prefix lengths so that we don't keep prefixLength at 0 until // both the start and limit indexes have moved. // At the same time, we find if s is one of the start and (limit-1) names, // and if not, exclude them from the actual binary search. if(0==strcmpAfterPrefix(s, names+toc[0].nameOffset, startPrefixLength)) { return 0; } ++start; --limit; if(0==strcmpAfterPrefix(s, names+toc[limit].nameOffset, limitPrefixLength)) { return limit; } while(start<limit) { int32_t i=(start+limit)/2; int32_t prefixLength=MIN(startPrefixLength, limitPrefixLength); int32_t cmp=strcmpAfterPrefix(s, names+toc[i].nameOffset, prefixLength); if(cmp<0) { limit=i; limitPrefixLength=prefixLength; } else if(cmp==0) { return i; } else { start=i+1; startPrefixLength=prefixLength; } } return -1; } class PrefixBinarySearchPackageLookup : public BinarySearchPackageLookup { public: PrefixBinarySearchPackageLookup(const DictionaryTriePerfTest &perf) : BinarySearchPackageLookup(perf) {} virtual void call(UErrorCode * /*pErrorCode*/) { int32_t count=pkg.getItemCount(); const char *itemNameChars=itemNames.data(); const char *name=itemNameChars; for(int32_t i=0; i<count; ++i) { if(prefixBinarySearch(name, itemNameChars, toc, count)<0) { fprintf(stderr, "item not found: %s\n", name); } name=strchr(name, 0)+1; } } }; static int32_t bytesTrieLookup(const char *s, const char *nameTrieBytes) { BytesTrie trie(nameTrieBytes); if(USTRINGTRIE_HAS_VALUE(trie.next(s, -1))) { return trie.getValue(); } else { return -1; } } class BytesTriePackageLookup : public PackageLookup { public: BytesTriePackageLookup(const DictionaryTriePerfTest &perf) : PackageLookup(perf) { IcuToolErrorCode errorCode("BinarySearchPackageLookup()"); builder=new BytesTrieBuilder(errorCode); int32_t count=pkg.getItemCount(); for(int32_t i=0; i<count; ++i) { // The Package class removes the "icudt46l/" prefix. // We restore that here for a fair performance test. // We store all full names so that we do not have to reconstruct them // in the call() function. const char *name=pkg.getItem(i)->name; int32_t offset=itemNames.length(); itemNames.append("icudt46l/", errorCode); itemNames.append(name, -1, errorCode); // As value, set the data item index. // In a real implementation, we would use that to get the // start and limit offset of the data item. StringPiece fullName(itemNames.toStringPiece()); fullName.remove_prefix(offset); builder->add(fullName, i, errorCode); // NUL-terminate the name for call() to find the next one. itemNames.append(0, errorCode); } int32_t length=builder->buildStringPiece(USTRINGTRIE_BUILD_SMALL, errorCode).length(); printf("size of BytesTrie: %6ld\n", (long)length); // count+1: +1 for the last-item limit offset which we should have always had printf("size of dataOffsets:%6ld\n", (long)((count+1)*4)); printf("total index size: %6ld\n", (long)(length+(count+1)*4)); } virtual ~BytesTriePackageLookup() { delete builder; } virtual void call(UErrorCode *pErrorCode) { int32_t count=pkg.getItemCount(); const char *nameTrieBytes=builder->buildStringPiece(USTRINGTRIE_BUILD_SMALL, *pErrorCode).data(); const char *name=itemNames.data(); for(int32_t i=0; i<count; ++i) { if(bytesTrieLookup(name, nameTrieBytes)<0) { fprintf(stderr, "item not found: %s\n", name); } name=strchr(name, 0)+1; } } protected: BytesTrieBuilder *builder; CharString itemNames; }; // Performance test function object. // Each subclass loads a dictionary text file // from the -s or --sourcedir path plus -f or --file-name. // For example, <ICU source dir>/source/data/brkitr/thaidict.txt. class DictLookup : public UPerfFunction { public: DictLookup(const DictionaryTriePerfTest &perfTest) : perf(perfTest) {} virtual long getOperationsPerIteration() { return perf.numTextLines; } protected: const DictionaryTriePerfTest &perf; }; // Closely imitate CompactTrieDictionary::matches(). // Note: CompactTrieDictionary::matches() is part of its trie implementation, // and while it loops over the text, it knows the current state. // By contrast, this implementation uses UCharsTrie API functions that have to // check the trie state each time and load/store state in the object. // (Whether it hasNext() and whether it is in the middle of a linear-match node.) static int32_t ucharsTrieMatches(UCharsTrie &trie, UText *text, int32_t textLimit, int32_t *lengths, int &count, int limit ) { UChar32 c=utext_next32(text); // Notes: // a) CompactTrieDictionary::matches() does not check for U_SENTINEL. // b) It also ignores non-BMP code points by casting to UChar! if(c<0) { return 0; } // Should be firstForCodePoint() but CompactTrieDictionary // handles only code units. UStringTrieResult result=trie.first(c); int32_t numChars=1; count=0; for(;;) { if(USTRINGTRIE_HAS_VALUE(result)) { if(count<limit) { // lengths[count++]=(int32_t)utext_getNativeIndex(text); lengths[count++]=numChars; // CompactTrieDictionary just counts chars too. } if(result==USTRINGTRIE_FINAL_VALUE) { break; } } else if(result==USTRINGTRIE_NO_MATCH) { break; } if(numChars>=textLimit) { // Note: Why do we have both a text limit and a UText that knows its length? break; } UChar32 c=utext_next32(text); // Notes: // a) CompactTrieDictionary::matches() does not check for U_SENTINEL. // b) It also ignores non-BMP code points by casting to UChar! if(c<0) { break; } ++numChars; // Should be nextForCodePoint() but CompactTrieDictionary // handles only code units. result=trie.next(c); } #if 0 // Note: CompactTrieDictionary::matches() comments say that it leaves the UText // after the longest prefix match and returns the number of characters // that were matched. if(index!=lastMatch) { utext_setNativeIndex(text, lastMatch); } return lastMatch-start; // However, it does not do either of these, so I am not trying to // imitate it (or its docs) 100%. #endif return numChars; } class UCharsTrieDictLookup : public DictLookup { public: UCharsTrieDictLookup(const DictionaryTriePerfTest &perfTest) : DictLookup(perfTest), trie(NULL) { IcuToolErrorCode errorCode("UCharsTrieDictLookup()"); builder=new UCharsTrieBuilder(errorCode); const ULine *lines=perf.getCachedLines(); int32_t numLines=perf.getNumLines(); for(int32_t i=0; i<numLines; ++i) { // Skip comment lines (start with a character below 'A'). if(lines[i].name[0]<0x41) { continue; } builder->add(UnicodeString(FALSE, lines[i].name, lines[i].len), 0, errorCode); } UnicodeString trieUChars; int32_t length=builder->buildUnicodeString(USTRINGTRIE_BUILD_SMALL, trieUChars, errorCode).length(); printf("size of UCharsTrie: %6ld bytes\n", (long)length*2); trie=builder->build(USTRINGTRIE_BUILD_SMALL, errorCode); } virtual ~UCharsTrieDictLookup() { delete builder; delete trie; } protected: UCharsTrieBuilder *builder; UCharsTrie *trie; }; class UCharsTrieDictMatches : public UCharsTrieDictLookup { public: UCharsTrieDictMatches(const DictionaryTriePerfTest &perfTest) : UCharsTrieDictLookup(perfTest) {} virtual void call(UErrorCode *pErrorCode) { UText text=UTEXT_INITIALIZER; int32_t lengths[20]; const ULine *lines=perf.getCachedLines(); int32_t numLines=perf.getNumLines(); for(int32_t i=0; i<numLines; ++i) { // Skip comment lines (start with a character below 'A'). if(lines[i].name[0]<0x41) { continue; } utext_openUChars(&text, lines[i].name, lines[i].len, pErrorCode); int32_t count=0; ucharsTrieMatches(*trie, &text, lines[i].len, lengths, count, UPRV_LENGTHOF(lengths)); if(count==0 || lengths[count-1]!=lines[i].len) { fprintf(stderr, "word %ld (0-based) not found\n", (long)i); } } } }; class UCharsTrieDictContains : public UCharsTrieDictLookup { public: UCharsTrieDictContains(const DictionaryTriePerfTest &perfTest) : UCharsTrieDictLookup(perfTest) {} virtual void call(UErrorCode * /*pErrorCode*/) { const ULine *lines=perf.getCachedLines(); int32_t numLines=perf.getNumLines(); for(int32_t i=0; i<numLines; ++i) { // Skip comment lines (which start with a character below 'A'). if(lines[i].name[0]<0x41) { continue; } if(!USTRINGTRIE_HAS_VALUE(trie->reset().next(lines[i].name, lines[i].len))) { fprintf(stderr, "word %ld (0-based) not found\n", (long)i); } } } }; static inline int32_t thaiCharToByte(UChar32 c) { if(0xe00<=c && c<=0xefe) { return c&0xff; } else if(c==0x2e) { return 0xff; } else { return -1; } } static UBool thaiWordToBytes(const UChar *s, int32_t length, CharString &str, UErrorCode &errorCode) { for(int32_t i=0; i<length; ++i) { UChar c=s[i]; int32_t b=thaiCharToByte(c); if(b>=0) { str.append((char)b, errorCode); } else { fprintf(stderr, "thaiWordToBytes(): unable to encode U+%04X as a byte\n", c); return FALSE; } } return TRUE; } class BytesTrieDictLookup : public DictLookup { public: BytesTrieDictLookup(const DictionaryTriePerfTest &perfTest) : DictLookup(perfTest), trie(NULL), noDict(FALSE) { IcuToolErrorCode errorCode("BytesTrieDictLookup()"); builder=new BytesTrieBuilder(errorCode); CharString str; const ULine *lines=perf.getCachedLines(); int32_t numLines=perf.getNumLines(); for(int32_t i=0; i<numLines; ++i) { // Skip comment lines (start with a character below 'A'). if(lines[i].name[0]<0x41) { continue; } if(!thaiWordToBytes(lines[i].name, lines[i].len, str.clear(), errorCode)) { fprintf(stderr, "thaiWordToBytes(): failed for word %ld (0-based)\n", (long)i); noDict=TRUE; break; } builder->add(str.toStringPiece(), 0, errorCode); } if(!noDict) { int32_t length=builder->buildStringPiece(USTRINGTRIE_BUILD_SMALL, errorCode).length(); printf("size of BytesTrie: %6ld bytes\n", (long)length); trie=builder->build(USTRINGTRIE_BUILD_SMALL, errorCode); } } virtual ~BytesTrieDictLookup() { delete builder; delete trie; } protected: BytesTrieBuilder *builder; BytesTrie *trie; UBool noDict; }; static int32_t bytesTrieMatches(BytesTrie &trie, UText *text, int32_t textLimit, int32_t *lengths, int &count, int limit ) { UChar32 c=utext_next32(text); if(c<0) { return 0; } UStringTrieResult result=trie.first(thaiCharToByte(c)); int32_t numChars=1; count=0; for(;;) { if(USTRINGTRIE_HAS_VALUE(result)) { if(count<limit) { // lengths[count++]=(int32_t)utext_getNativeIndex(text); lengths[count++]=numChars; // CompactTrieDictionary just counts chars too. } if(result==USTRINGTRIE_FINAL_VALUE) { break; } } else if(result==USTRINGTRIE_NO_MATCH) { break; } if(numChars>=textLimit) { break; } UChar32 c=utext_next32(text); if(c<0) { break; } ++numChars; result=trie.next(thaiCharToByte(c)); } return numChars; } class BytesTrieDictMatches : public BytesTrieDictLookup { public: BytesTrieDictMatches(const DictionaryTriePerfTest &perfTest) : BytesTrieDictLookup(perfTest) {} virtual void call(UErrorCode *pErrorCode) { if(noDict) { return; } UText text=UTEXT_INITIALIZER; int32_t lengths[20]; const ULine *lines=perf.getCachedLines(); int32_t numLines=perf.getNumLines(); for(int32_t i=0; i<numLines; ++i) { // Skip comment lines (start with a character below 'A'). if(lines[i].name[0]<0x41) { continue; } utext_openUChars(&text, lines[i].name, lines[i].len, pErrorCode); int32_t count=0; bytesTrieMatches(*trie, &text, lines[i].len, lengths, count, UPRV_LENGTHOF(lengths)); if(count==0 || lengths[count-1]!=lines[i].len) { fprintf(stderr, "word %ld (0-based) not found\n", (long)i); } } } }; class BytesTrieDictContains : public BytesTrieDictLookup { public: BytesTrieDictContains(const DictionaryTriePerfTest &perfTest) : BytesTrieDictLookup(perfTest) {} virtual void call(UErrorCode * /*pErrorCode*/) { if(noDict) { return; } const ULine *lines=perf.getCachedLines(); int32_t numLines=perf.getNumLines(); for(int32_t i=0; i<numLines; ++i) { const UChar *line=lines[i].name; // Skip comment lines (start with a character below 'A'). if(line[0]<0x41) { continue; } UStringTrieResult result=trie->first(thaiCharToByte(line[0])); int32_t lineLength=lines[i].len; for(int32_t j=1; j<lineLength; ++j) { if(!USTRINGTRIE_HAS_NEXT(result)) { fprintf(stderr, "word %ld (0-based) not found\n", (long)i); break; } result=trie->next(thaiCharToByte(line[j])); } if(!USTRINGTRIE_HAS_VALUE(result)) { fprintf(stderr, "word %ld (0-based) not found\n", (long)i); } } } }; UPerfFunction *DictionaryTriePerfTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) { if(hasFile()) { switch(index) { case 0: name="ucharstriematches"; if(exec) { return new UCharsTrieDictMatches(*this); } break; case 1: name="ucharstriecontains"; if(exec) { return new UCharsTrieDictContains(*this); } break; case 2: name="bytestriematches"; if(exec) { return new BytesTrieDictMatches(*this); } break; case 3: name="bytestriecontains"; if(exec) { return new BytesTrieDictContains(*this); } break; default: name=""; break; } } else { if(index==0 && exec) { puts("Running BytesTrie perf tests on the .dat package file from the --sourcedir.\n" "For UCharsTrie perf tests on a dictionary text file, specify the -f or --file-name.\n"); } switch(index) { case 0: name="simplebinarysearch"; if(exec) { return new BinarySearchPackageLookup(*this); } break; case 1: name="prefixbinarysearch"; if(exec) { return new PrefixBinarySearchPackageLookup(*this); } break; case 2: name="bytestrie"; if(exec) { return new BytesTriePackageLookup(*this); } break; default: name=""; break; } } return NULL; } int main(int argc, const char *argv[]) { IcuToolErrorCode errorCode("dicttrieperf main()"); DictionaryTriePerfTest test(argc, argv, errorCode); if(errorCode.isFailure()) { fprintf(stderr, "DictionaryTriePerfTest() failed: %s\n", errorCode.errorName()); test.usage(); return errorCode.reset(); } if(!test.run()) { fprintf(stderr, "FAILED: Tests could not be run, please check the arguments.\n"); return -1; } return 0; }