Skip to content

Dictionary: Improve Dictionary Search Efficiency - #491

Open
nithin-aashik-mcw wants to merge 2 commits into
cmusphinx:mainfrom
nithin-aashik-mcw:decoder/add_word
Open

Dictionary: Improve Dictionary Search Efficiency#491
nithin-aashik-mcw wants to merge 2 commits into
cmusphinx:mainfrom
nithin-aashik-mcw:decoder/add_word

Conversation

@nithin-aashik-mcw

Copy link
Copy Markdown

Changes Proposed

  • Optimized the root channel lookup in create_search_channels() by replacing the repeated linear search with a lookup table indexed by (ciphone, ci2phone).
  • Previously, each dictionary word performed an O(n_root_chan) scan to locate an existing root channel, increasing the time required to construct the search tree.
  • Added a dynamically allocated lookup table based on the number of Context-Independent (CI) phones in the acoustic model, enabling O(1) root channel lookup.
  • The optimization only changes the lookup mechanism used during search tree construction. The search tree structure and recognition behavior remain unchanged.

Validation

  • Wrote a benchmark for Decoder.add_word(update=True) to evaluate the performance of rebuilding the search tree.
  • The benchmark identified create_search_channels() as a performance bottleneck, specifically the repeated linear search for existing root channels.
  • Reran the benchmark after implementing the optimization and verified the performance improvement.
  • All existing functionality tests pass successfully with no functional regressions observed.

Performance Results

Benchmark Metric Before After Improvement
Decoder.add_word(update=True) ms/call 62.080 51.899 16.4% faster

@nithin-aashik-mcw
nithin-aashik-mcw marked this pull request as ready for review August 7, 2026 11:44
@lenzo-ka

Copy link
Copy Markdown
Contributor

Nice optimization — replacing the O(n_root_chan) linear scan in create_search_channels() with an O(1) root_lookup[ciphone][ci2phone] table is a clean win. The lookup is logically equivalent to the original: the table is zero-initialized so NULL means "not found", it is populated exactly when a new root channel is created, and single-phone words continue before the lookup (ngram_search_fwdtree.c:202) so the ciphone/ci2phone indices are always valid CI phones in [0, n_ci). The root_chan_t *** type matches ckd_calloc_2d(n_ci, n_ci, sizeof(root_chan_t *)).

One issue to fix before merge, though: root_lookup is never freed.

It is allocated in init_search_tree() but deinit_search_tree() (which frees every other search-tree array) does not free it. deinit_search_tree() runs both at teardown (ngram_fwdtree_deinit) and before every rebuild (ngram_fwdtree_reinit), so the whole n_ci × n_ci table leaks on every search-tree build — including each Decoder.add_word(update=True), which is exactly the path this PR optimizes.

Confirmed under AddressSanitizer/LeakSanitizer (Linux/GCC), single decoder lifecycle via test_fwdtree:

Direct leak of 336 byte(s) in 1 object(s)      (row pointers, n_ci=42)
Indirect leak of 14112 byte(s) in 1 object(s)  (42 x 42 table)
SUMMARY: AddressSanitizer: 14448 byte(s) leaked in 2 allocation(s).
    #3 init_search_tree src/ngram_search_fwdtree.c:85

That is ~14 KB per build, and it grows without bound across repeated add_word(update=True).

The fix is one line, added to deinit_search_tree() alongside the other frees:

ckd_free_2d(ngs->root_lookup);
ngs->root_lookup = NULL;

With that added, the LeakSanitizer run comes back clean. deinit_search_tree() is the right place since it is paired with the init_search_tree() that allocates the table, covering both the rebuild and teardown paths.

@nithin-aashik-mcw

Copy link
Copy Markdown
Author

Good catch. root_lookup was missing from deinit_search_tree(), causing a leak on every search-tree rebuild. I've added the corresponding ckd_free_2d() and verified that the leak is resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants