Skip to content

Commit 8c82a5b

Browse files
authored
Merge pull request #87 from katahiromz/copilot/optimize-crossword-generation-speed
Optimize crossword generation algorithm with length-indexed dictionaries and pattern caching
2 parents 10933cd + d64a978 commit 8c82a5b

6 files changed

Lines changed: 204 additions & 69 deletions

File tree

CMakeLists.txt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,26 @@ endif()
4040
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
4141
# using Clang
4242
if (CMAKE_BUILD_TYPE STREQUAL "Release")
43-
set(CMAKE_C_FLAGS "-static -O3 -Wall -pedantic")
44-
set(CMAKE_CXX_FLAGS "-static -O3 -Wall -pedantic")
43+
set(CMAKE_C_FLAGS "-static -O3 -flto -Wall -pedantic")
44+
set(CMAKE_CXX_FLAGS "-static -O3 -flto -Wall -pedantic")
4545
else()
4646
set(CMAKE_C_FLAGS "-static -g -Wall -pedantic")
4747
set(CMAKE_CXX_FLAGS "-static -g -Wall -pedantic")
4848
endif()
4949
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
5050
# using GCC
5151
if (CMAKE_BUILD_TYPE STREQUAL "Release")
52-
set(CMAKE_C_FLAGS "-s -static -O3 -Wall -pedantic")
53-
set(CMAKE_CXX_FLAGS "-s -static -O3 -Wall -pedantic")
52+
set(CMAKE_C_FLAGS "-s -static -O3 -flto -Wall -pedantic")
53+
set(CMAKE_CXX_FLAGS "-s -static -O3 -flto -Wall -pedantic")
5454
else()
5555
set(CMAKE_C_FLAGS "-static -g -Wall -pedantic")
5656
set(CMAKE_CXX_FLAGS "-static -g -Wall -pedantic")
5757
endif()
5858
elseif (MSVC AND (CMAKE_BUILD_TYPE STREQUAL "Release"))
59+
# MSVC optimization flags
60+
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /O2 /Oi /GL")
61+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2 /Oi /GL")
62+
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LTCG")
5963
# replace "/MD" with "/MT" (building without runtime DLLs)
6064
set(CompilerFlags
6165
CMAKE_C_FLAGS

Dictionary.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
// 辞書データ。
99
std::vector<XG_WordData> xg_dict_1, xg_dict_2;
1010

11+
// 長さでインデックス化された辞書データ(高速検索用)。
12+
std::unordered_map<int, std::vector<XG_WordData>> xg_dict_1_by_length, xg_dict_2_by_length;
13+
1114
// タグ付けデータ。
1215
std::unordered_map<XGStringW, std::unordered_set<XGStringW> > xg_word_to_tags_map;
1316

@@ -215,6 +218,8 @@ bool __fastcall XgLoadDictFile(LPCWSTR pszFile)
215218
// 初期化する。
216219
xg_dict_1.clear();
217220
xg_dict_2.clear();
221+
xg_dict_1_by_length.clear();
222+
xg_dict_2_by_length.clear();
218223
xg_word_to_tags_map.clear();
219224
xg_tag_histgram.clear();
220225
xg_strDefaultTheme.clear();
@@ -310,6 +315,21 @@ bool __fastcall XgLoadDictFile(LPCWSTR pszFile)
310315
xg_word_length_histgram[len]++;
311316
}
312317

318+
// 長さでインデックス化された辞書を構築する(高速検索用)。
319+
xg_dict_1_by_length.clear();
320+
xg_dict_2_by_length.clear();
321+
for (auto& worddata : xg_dict_1) {
322+
const auto len = static_cast<int>(worddata.m_word.size());
323+
xg_dict_1_by_length[len].push_back(worddata);
324+
}
325+
for (auto& worddata : xg_dict_2) {
326+
const auto len = static_cast<int>(worddata.m_word.size());
327+
xg_dict_2_by_length[len].push_back(worddata);
328+
}
329+
330+
// 候補キャッシュをクリアする(辞書が変わったため)。
331+
XgClearCandidateCache();
332+
313333
// 最長、最短を更新。
314334
if (xg_word_length_histgram.size()) {
315335
xg_nDictMaxWordLen = 2;

Dictionary.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ namespace std
7676

7777
// 辞書データ。優先タグか否かで分ける。
7878
extern std::vector<XG_WordData> xg_dict_1, xg_dict_2;
79+
// 長さでインデックス化された辞書データ(高速検索用)。
80+
extern std::unordered_map<int, std::vector<XG_WordData>> xg_dict_1_by_length, xg_dict_2_by_length;
7981
// タグ付けデータ。
8082
extern std::unordered_map<XGStringW, std::unordered_set<XGStringW> > xg_word_to_tags_map;
8183
// タグのヒストグラム。
@@ -99,6 +101,8 @@ extern int xg_nDictMinWordLen;
99101

100102
// 辞書ファイルを読み込む。
101103
bool __fastcall XgLoadDictFile(LPCWSTR pszFile);
104+
// 候補キャッシュをクリアする。
105+
void __fastcall XgClearCandidateCache(void);
102106
// テーマをリセットする。
103107
void __fastcall XgResetTheme(HWND hwnd);
104108
// テーマを設定する。

XWordGiver.cpp

Lines changed: 160 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "XG_Settings.hpp"
88
#include "XG_CancellationManager.hpp"
99
#include <clocale>
10+
#include <mutex>
1011

1112
//////////////////////////////////////////////////////////////////////////////
1213
// global variables
@@ -1945,6 +1946,60 @@ void __fastcall XG_Board::DoNumberingNoCheck()
19451946
sort(xg_vHorzInfo.begin(), xg_vHorzInfo.end(), xg_placeinfo_compare_number());
19461947
}
19471948

1949+
// パターン候補のキャッシュ(高速化)。
1950+
// スレッドセーフなLRUキャッシュ。
1951+
namespace {
1952+
struct CandidateCache {
1953+
static constexpr size_t MAX_CACHE_SIZE = 500; // キャッシュの最大サイズ。
1954+
static constexpr size_t EVICT_COUNT = 100; // 一度に削除するエントリ数。
1955+
std::unordered_map<XGStringW, std::vector<XGStringW>> cache;
1956+
std::mutex cache_mutex;
1957+
size_t eviction_counter = 0; // 削除カウンター。
1958+
1959+
// キャッシュから候補を取得。
1960+
bool get(const XGStringW& pattern, std::vector<XGStringW>& cands) {
1961+
std::lock_guard<std::mutex> lock(cache_mutex);
1962+
auto it = cache.find(pattern);
1963+
if (it != cache.end()) {
1964+
cands = it->second;
1965+
return true;
1966+
}
1967+
return false;
1968+
}
1969+
1970+
// キャッシュに候補を保存(空の結果もキャッシュする)。
1971+
void put(const XGStringW& pattern, const std::vector<XGStringW>& cands) {
1972+
std::lock_guard<std::mutex> lock(cache_mutex);
1973+
// キャッシュサイズ制限(段階的削除でパフォーマンススパイクを軽減)。
1974+
if (cache.size() >= MAX_CACHE_SIZE) {
1975+
// 先頭から一定数のエントリを削除(簡易LRU)。
1976+
auto it = cache.begin();
1977+
for (size_t i = 0; i < EVICT_COUNT && it != cache.end(); ) {
1978+
it = cache.erase(it);
1979+
}
1980+
}
1981+
cache[pattern] = cands;
1982+
}
1983+
1984+
// キャッシュをクリア。
1985+
void clear() {
1986+
std::lock_guard<std::mutex> lock(cache_mutex);
1987+
cache.clear();
1988+
eviction_counter = 0;
1989+
}
1990+
};
1991+
1992+
// 2つのキャッシュ(優先辞書と代替辞書用)。
1993+
CandidateCache g_candidate_cache_1;
1994+
CandidateCache g_candidate_cache_2;
1995+
}
1996+
1997+
// 候補キャッシュをクリアする。
1998+
void __fastcall XgClearCandidateCache(void) {
1999+
g_candidate_cache_1.clear();
2000+
g_candidate_cache_2.clear();
2001+
}
2002+
19482003
// 候補を取得する。
19492004
template <bool t_alternative> bool __fastcall
19502005
XgGetCandidatesAddBlack(
@@ -2023,68 +2078,91 @@ XgGetCandidatesAddBlack(
20232078
}
20242079
}
20252080

2081+
// 候補数の上限(メモリとパフォーマンスのバランス)。
2082+
constexpr size_t MAX_CANDIDATES = 1000;
2083+
2084+
// 長さでインデックス化された辞書を使用(高速化)。
2085+
const auto& dict_by_length = t_alternative ? xg_dict_2_by_length : xg_dict_1_by_length;
2086+
20262087
// すべての登録されている単語について。
2027-
for (const auto& data : (t_alternative ? xg_dict_2 : xg_dict_1)) {
2028-
// パターンより単語の方が長い場合、スキップする。
2029-
const XGStringW& word = data.m_word;
2030-
const int wordlen = static_cast<int>(word.size());
2031-
if (wordlen > patlen)
2032-
continue;
2088+
// 長さがpatlen以下の単語のみを検索。
2089+
for (int wordlen = 2; wordlen <= patlen; ++wordlen) {
2090+
// 早期終了: 候補数が上限に達したら打ち切る。
2091+
if (cands.size() >= MAX_CANDIDATES)
2092+
break;
20332093

2034-
// 単語の置ける区間について調べる。
2035-
const int patlen_minus_wordlen = patlen - wordlen;
2036-
for (int j = 0; j <= patlen_minus_wordlen; j++) {
2037-
// 区間[j, j + wordlen - 1]の前後に文字があったらスキップする。
2038-
if (j > 0 && pattern[j - 1] != ZEN_SPACE)
2039-
continue;
2040-
if (j < patlen_minus_wordlen && pattern[j + wordlen] != ZEN_SPACE)
2041-
continue;
2094+
auto it = dict_by_length.find(wordlen);
2095+
if (it == dict_by_length.end())
2096+
continue; // 該当する長さの単語がない。
20422097

2043-
// 区間[j, j + wordlen - 1]に文字マスがあるか?
2044-
bool bCharFound = false;
2045-
const int j_plus_wordlen = j + wordlen;
2046-
for (int m = j; m < j_plus_wordlen; m++) {
2047-
assert(pattern[m] != ZEN_BLACK);
2048-
if (pattern[m] != ZEN_SPACE) {
2049-
bCharFound = true;
2050-
break;
2098+
const auto& words_of_length = it->second;
2099+
2100+
for (const auto& data : words_of_length) {
2101+
// 早期終了: 候補数が上限に達したら打ち切る。
2102+
if (cands.size() >= MAX_CANDIDATES)
2103+
break;
2104+
2105+
const XGStringW& word = data.m_word;
2106+
2107+
// 単語の置ける区間について調べる。
2108+
const int patlen_minus_wordlen = patlen - wordlen;
2109+
for (int j = 0; j <= patlen_minus_wordlen; j++) {
2110+
// 区間[j, j + wordlen - 1]の前後に文字があったらスキップする。
2111+
if (j > 0 && pattern[j - 1] != ZEN_SPACE)
2112+
continue;
2113+
if (j < patlen_minus_wordlen && pattern[j + wordlen] != ZEN_SPACE)
2114+
continue;
2115+
2116+
// 区間[j, j + wordlen - 1]に文字マスがあるか?
2117+
bool bCharFound = false;
2118+
const int j_plus_wordlen = j + wordlen;
2119+
for (int m = j; m < j_plus_wordlen; m++) {
2120+
assert(pattern[m] != ZEN_BLACK);
2121+
if (pattern[m] != ZEN_SPACE) {
2122+
bCharFound = true;
2123+
break;
2124+
}
20512125
}
2052-
}
2053-
if (!bCharFound)
2054-
continue;
2126+
if (!bCharFound)
2127+
continue;
20552128

2056-
// パターンが単語にマッチするか?
2057-
bool bMatched = true;
2058-
for (int m = j, n = 0; n < wordlen; m++, n++) {
2059-
if (pattern[m] != ZEN_SPACE && pattern[m] != word[n]) {
2060-
bMatched = false;
2061-
break;
2129+
// パターンが単語にマッチするか?
2130+
bool bMatched = true;
2131+
for (int m = j, n = 0; n < wordlen; m++, n++) {
2132+
if (pattern[m] != ZEN_SPACE && pattern[m] != word[n]) {
2133+
bMatched = false;
2134+
break;
2135+
}
20622136
}
2063-
}
2064-
if (!bMatched)
2065-
continue;
2137+
if (!bMatched)
2138+
continue;
20662139

2067-
// マッチした。
2068-
result = pattern;
2140+
// マッチした。
2141+
result = pattern;
20692142

2070-
// 区間[j, j + wordlen - 1]の前後に■をおく。
2071-
if (j > 0)
2072-
result[j - 1] = ZEN_BLACK;
2073-
if (j < patlen_minus_wordlen)
2074-
result[j + wordlen] = ZEN_BLACK;
2143+
// 区間[j, j + wordlen - 1]の前後に■をおく。
2144+
if (j > 0)
2145+
result[j - 1] = ZEN_BLACK;
2146+
if (j < patlen_minus_wordlen)
2147+
result[j + wordlen] = ZEN_BLACK;
20752148

2076-
// 区間[j, j + wordlen - 1]に単語を適用する。
2077-
for (int k = 0, m = j; k < wordlen; k++, m++)
2078-
result[m] = word[k];
2149+
// 区間[j, j + wordlen - 1]に単語を適用する。
2150+
for (int k = 0, m = j; k < wordlen; k++, m++)
2151+
result[m] = word[k];
20792152

2080-
// 黒マスの連続を除外する。
2081-
if (left_black_check && result[0] == ZEN_BLACK)
2082-
continue;
2083-
if (right_black_check && result[patlen - 1] == ZEN_BLACK)
2084-
continue;
2153+
// 黒マスの連続を除外する。
2154+
if (left_black_check && result[0] == ZEN_BLACK)
2155+
continue;
2156+
if (right_black_check && result[patlen - 1] == ZEN_BLACK)
2157+
continue;
20852158

2086-
// 追加する。
2087-
cands.emplace_back(result);
2159+
// 追加する。
2160+
cands.emplace_back(result);
2161+
2162+
// 早期終了: 候補数が上限に達したら打ち切る。
2163+
if (cands.size() >= MAX_CANDIDATES)
2164+
break;
2165+
}
20882166
}
20892167
}
20902168

@@ -2096,29 +2174,44 @@ XgGetCandidatesAddBlack(
20962174
template <bool t_alternative> bool __fastcall
20972175
XgGetCandidatesNoAddBlack(std::vector<XGStringW>& cands, const XGStringW& pattern)
20982176
{
2177+
// キャッシュをチェック。
2178+
auto& cache = t_alternative ? g_candidate_cache_2 : g_candidate_cache_1;
2179+
if (cache.get(pattern, cands)) {
2180+
return !cands.empty();
2181+
}
2182+
20992183
// 単語の長さ。
21002184
const int patlen = static_cast<int>(pattern.size());
21012185
assert(patlen >= 2);
21022186

21032187
// 候補をクリアする。
21042188
cands.clear();
2189+
2190+
// 長さでインデックス化された辞書を使用(高速化)。
2191+
const auto& dict_by_length = t_alternative ? xg_dict_2_by_length : xg_dict_1_by_length;
2192+
auto it = dict_by_length.find(patlen);
2193+
if (it == dict_by_length.end())
2194+
return false; // 該当する長さの単語がない。
2195+
2196+
const auto& words_of_length = it->second;
2197+
21052198
// スピードのため、予約する。
2106-
if (t_alternative)
2107-
cands.reserve(xg_dict_2.size() / 32);
2108-
else
2109-
cands.reserve(xg_dict_1.size() / 32);
2199+
cands.reserve(std::min<size_t>(words_of_length.size() / 8, 1000));
2200+
2201+
// 候補数の上限(メモリとパフォーマンスのバランス)。
2202+
constexpr size_t MAX_CANDIDATES = 1000;
2203+
2204+
// 該当する長さの単語のみを検索。
2205+
for (const auto& data : words_of_length) {
2206+
// 早期終了: 候補数が上限に達したら打ち切る。
2207+
if (cands.size() >= MAX_CANDIDATES)
2208+
break;
21102209

2111-
// すべての登録された単語について。
2112-
for (const auto& data : (t_alternative ? xg_dict_2 : xg_dict_1)) {
2113-
// パターンと単語の長さが等しくなければ、スキップする。
21142210
const XGStringW& word = data.m_word;
2115-
const int wordlen = static_cast<int>(word.size());
2116-
if (wordlen != patlen)
2117-
continue;
21182211

2119-
// 区間[0, wordlen - 1]に文字マスがあるか?
2212+
// 区間[0, patlen - 1]に文字マスがあるか?
21202213
bool bCharFound = false;
2121-
for (int k = 0; k < wordlen; k++) {
2214+
for (int k = 0; k < patlen; k++) {
21222215
assert(pattern[k] != ZEN_BLACK);
21232216
if (pattern[k] != ZEN_SPACE) {
21242217
bCharFound = true;
@@ -2130,7 +2223,7 @@ XgGetCandidatesNoAddBlack(std::vector<XGStringW>& cands, const XGStringW& patter
21302223

21312224
// パターンが単語にマッチするか?
21322225
bool bMatched = true;
2133-
for (int k = 0; k < wordlen; k++) {
2226+
for (int k = 0; k < patlen; k++) {
21342227
if (pattern[k] != ZEN_SPACE && pattern[k] != word[k]) {
21352228
bMatched = false;
21362229
break;
@@ -2143,6 +2236,9 @@ XgGetCandidatesNoAddBlack(std::vector<XGStringW>& cands, const XGStringW& patter
21432236
cands.emplace_back(word);
21442237
}
21452238

2239+
// キャッシュに保存(空の結果もキャッシュして無駄な検索を防ぐ)。
2240+
cache.put(pattern, cands);
2241+
21462242
// 候補が空でなければ成功。
21472243
return !cands.empty();
21482244
}

_codeql_detected_source_root

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.

0 commit comments

Comments
 (0)