Skip to content

Commit 19dd0c6

Browse files
Merge pull request #102 from cpprefjp/performance_up
Markdown → HTML変換を高速化
2 parents bfabeff + b15e9a7 commit 19dd0c6

2 files changed

Lines changed: 33 additions & 15 deletions

File tree

markdown_to_html/qualified_fenced_code.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,28 @@ def _qualify_link(*xs):
105105
}
106106

107107

108-
class Qualifier(object):
108+
# 修飾行のパース用正規表現。QualifyDictionary のコマンド一覧は不変なので、
109+
# モジュール読み込み時に一度だけ構築し Qualifier ごとの再コンパイルを避ける。
110+
_QUALIFY_DICTIONARY = QualifyDictionary()
111+
_QUALIFY_LINE_RE = re.compile(
112+
r'^[ \t]*\*[ \t]+(?P<target>.*?)(?P<commands>({commands})+)$'.format(
113+
commands='|'.join(r'(\[{cmd}(\]|.*?\]))'.format(cmd=cmd) for cmd in _QUALIFY_DICTIONARY.qualify_dic)))
109114

110-
"""修飾1個分のデータを保持するクラス"""
115+
# 同一の修飾行から作られる Qualifier はコンパイル済み正規表現ごと使い回せる
116+
# (target/commands は行文字列から決まる不変値で find_match は状態を持たない)。
117+
# GLOBAL_QUALIFY_LIST の各行は全コードブロックで共通なので、ここで共有すること
118+
# でコードブロック単位に発生していた大量の正規表現コンパイルを削減する。
119+
# パースに失敗した行は None を記憶して再試行しない。
120+
_QUALIFIER_CACHE = {}
111121

112-
def __init__(self, line, qdic):
113-
command_res = [r'(\[{cmd}(\]|.*?\]))'.format(cmd=cmd) for cmd in qdic.qualify_dic]
114122

115-
qualify_re_str = r'^[ \t]*\*[ \t]+(?P<target>.*?)(?P<commands>({commands})+)$'.format(
116-
commands='|'.join(command_res))
117-
qualify_re = re.compile(qualify_re_str)
123+
class Qualifier(object):
124+
125+
"""修飾1個分のデータを保持するクラス"""
118126

127+
def __init__(self, line, qdic=None):
119128
# parsing
120-
m = qualify_re.search(line)
129+
m = _QUALIFY_LINE_RE.search(line)
121130
if not m:
122131
raise ValueError('Failed parse')
123132
self.target = m.group('target')
@@ -156,19 +165,26 @@ def find_match(self, code):
156165
class QualifierList(object):
157166

158167
def __init__(self, lines):
159-
self._qdic = QualifyDictionary()
168+
self._qdic = _QUALIFY_DICTIONARY
160169

161-
# Qualifier を作るが、エラーになったデータは取り除く
170+
# Qualifier を作るが、エラーになったデータは取り除く。同一行はキャッシュ
171+
# から共有し正規表現の再コンパイルを避ける (エラー行は None を記憶)。
162172
def unique(xs):
163173
seen = set()
164174
results = []
165175
for x in xs:
166-
if x not in seen:
167-
seen.add(x)
176+
if x in seen:
177+
continue
178+
seen.add(x)
179+
q = _QUALIFIER_CACHE.get(x, False)
180+
if q is False:
168181
try:
169-
results.append(Qualifier(x, self._qdic))
182+
q = Qualifier(x)
170183
except Exception:
171-
pass
184+
q = None
185+
_QUALIFIER_CACHE[x] = q
186+
if q is not None:
187+
results.append(q)
172188
return results
173189

174190
self._qs = unique(lines)

run.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
TARGET_PREFIX = [x[len('--prefix='):] for x in sys.argv[2:] if x.startswith('--prefix=')]
3434
TARGET_PREFIX = TARGET_PREFIX[0] if TARGET_PREFIX else ''
3535
CONCURRENCY = [int(x[len('--concurrency='):]) for x in sys.argv[2:] if x.startswith('--concurrency=')]
36-
CONCURRENCY = CONCURRENCY[0] if CONCURRENCY else 2
36+
# 明示指定がなければ CPU コア数を使う。変換は CPU バウンドで実コア数まで
37+
# ほぼ線形にスケールする (実測でデフォルト 2 に対し 16 コアで約 4.9 倍高速)。
38+
CONCURRENCY = CONCURRENCY[0] if CONCURRENCY else (os.cpu_count() or 2)
3739

3840
if settings.CACHEBUST_TYPE == 'none':
3941
_CACHEBUST = ''

0 commit comments

Comments
 (0)