Optimize hyphen position lookup O(n)->O(log n) and add named constants

Performance:
- ekp--hyphenate-p: O(n) cl-find -> O(log n) binary search
- is_hyphen_break: O(n) linear -> O(log n) binary search
- is_hyphen_pos: O(n) linear -> O(log n) binary search

In O(n²) DP loop, this reduces worst-case complexity from O(n³) to O(n² log n)

Code quality:
- Add ekp-consecutive-hyphen-penalty (was hardcoded 100)
- Add ekp-forced-break-penalty (was hardcoded 10000)
- Add ekp-last-line-short-penalty (was hardcoded 50)
- Update ekp-looseness docstring noting partial implementation

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-25 11:15:42 +00:00
parent fb58a9b19a
commit b70affb9ef
2 changed files with 60 additions and 16 deletions

35
ekp.el
View File

@ -69,11 +69,24 @@ Set to nil to force pure Elisp implementation.")
(defvar ekp-adjacent-fitness-penalty 100 (defvar ekp-adjacent-fitness-penalty 100
"Penalty when adjacent lines differ in tightness by >1 class.") "Penalty when adjacent lines differ in tightness by >1 class.")
(defvar ekp-consecutive-hyphen-penalty 100
"Base penalty multiplier for consecutive hyphenated lines.
Actual penalty = this × count², encouraging spread of hyphens.")
(defvar ekp-forced-break-penalty 10000
"Base penalty for forced breaks where no valid break exists.
High value ensures forced breaks are last resort.")
(defvar ekp-last-line-short-penalty 50
"Penalty multiplier for underfilled last lines.
Applied as: this × (1 - fill-ratio) when fill < ekp-last-line-min-ratio.")
(defvar ekp-last-line-min-ratio 0.5 (defvar ekp-last-line-min-ratio 0.5
"Minimum fill ratio for last line (0.0-1.0).") "Minimum fill ratio for last line (0.0-1.0).")
(defvar ekp-looseness 0 (defvar ekp-looseness 0
"Target line count offset: 0=optimal, +1=looser, -1=tighter.") "Target line count offset: 0=optimal, +1=looser, -1=tighter.
Note: Full looseness requires tracking multiple paths (not yet implemented).")
;;;; Paragraph Cache Structure ;;;; Paragraph Cache Structure
;; ;;
@ -460,7 +473,8 @@ Returns total demerits for this break."
;; Consecutive hyphen penalty (quadratic growth) ;; Consecutive hyphen penalty (quadratic growth)
(hyphen-count (if end-with-hyphenp (1+ prev-hyphen-count) 0)) (hyphen-count (if end-with-hyphenp (1+ prev-hyphen-count) 0))
(with-hyphen (if end-with-hyphenp (with-hyphen (if end-with-hyphenp
(+ with-fitness (* 100 hyphen-count hyphen-count)) (+ with-fitness (* ekp-consecutive-hyphen-penalty
hyphen-count hyphen-count))
with-fitness))) with-fitness)))
with-hyphen)) with-hyphen))
@ -504,9 +518,18 @@ Returns (:badness NUM :fitness NUM :gaps LIST :adjustment NUM :flexibility NUM).
(defun ekp--hyphenate-p (hyphen-positions n) (defun ekp--hyphenate-p (hyphen-positions n)
"Return non-nil if position N ends with hyphenation. "Return non-nil if position N ends with hyphenation.
HYPHEN-POSITIONS is a vector of indices where hyphenation can occur." HYPHEN-POSITIONS is a sorted vector of indices where hyphenation can occur.
Uses binary search for O(log n) lookup instead of O(n) linear search."
(and hyphen-positions (and hyphen-positions
(cl-find n hyphen-positions))) (> (length hyphen-positions) 0)
(let ((lo 0)
(hi (1- (length hyphen-positions))))
(while (< lo hi)
(let ((mid (/ (+ lo hi) 2)))
(if (< (aref hyphen-positions mid) n)
(setq lo (1+ mid))
(setq hi mid))))
(= (aref hyphen-positions lo) n))))
;;;; Dynamic Programming Line Breaking ;;;; Dynamic Programming Line Breaking
@ -552,7 +575,7 @@ Uses PARA's stored glue params for consistency."
(rest-pixel (- line-pixel ideal-pixel))) (rest-pixel (- line-pixel ideal-pixel)))
(when hyphenate-p (cl-incf ideal-pixel hyphen-pixel)) (when hyphenate-p (cl-incf ideal-pixel hyphen-pixel))
;; Force break with high demerits ;; Force break with high demerits
(aset demerits break-pos (+ 10000 (expt rest-pixel 2))) (aset demerits break-pos (+ ekp-forced-break-penalty (expt rest-pixel 2)))
(aset rests break-pos rest-pixel) (aset rests break-pos rest-pixel)
(aset backptrs break-pos i) (aset backptrs break-pos i)
(aset fitness-classes break-pos 3) ; very loose (aset fitness-classes break-pos 3) ; very loose
@ -582,7 +605,7 @@ Returns (demerits gaps fitness new-hyphen-count)."
(let* ((fill-ratio (/ (float ideal-pixel) line-pixel)) (let* ((fill-ratio (/ (float ideal-pixel) line-pixel))
;; Penalize if last line is too short ;; Penalize if last line is too short
(badness (if (< fill-ratio ekp-last-line-min-ratio) (badness (if (< fill-ratio ekp-last-line-min-ratio)
(* 50 (- 1.0 fill-ratio)) (* ekp-last-line-short-penalty (- 1.0 fill-ratio))
0)) 0))
(dem (expt (+ ekp-line-penalty badness) 2))) (dem (expt (+ ekp-line-penalty badness) 2)))
(list dem nil 1 0))) (list dem nil 1 0)))

View File

@ -83,14 +83,26 @@ static inline double compute_demerits(double badness, int32_t penalty,
/* /*
* Check if position is a hyphenation break * Check if position is a hyphenation break
* Uses binary search for O(log n) lookup (positions are sorted)
*/ */
static inline bool is_hyphen_break(ekp_paragraph_t *p, size_t pos) static inline bool is_hyphen_break(ekp_paragraph_t *p, size_t pos)
{ {
for (size_t i = 0; i < p->hyphen_count; i++) { if (p->hyphen_count == 0)
if ((size_t)p->hyphen_positions[i] == pos)
return true;
}
return false; return false;
/* Binary search in sorted hyphen_positions */
size_t lo = 0;
size_t hi = p->hyphen_count - 1;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if ((size_t)p->hyphen_positions[mid] < pos)
lo = mid + 1;
else
hi = mid;
}
return (size_t)p->hyphen_positions[lo] == pos;
} }
/* /*
@ -418,13 +430,22 @@ void ekp_result_destroy(ekp_result_t *r)
static inline bool is_hyphen_pos(const int32_t *positions, size_t count, int32_t pos) static inline bool is_hyphen_pos(const int32_t *positions, size_t count, int32_t pos)
{ {
for (size_t i = 0; i < count; i++) { if (count == 0)
if (positions[i] == pos)
return true;
if (positions[i] > pos)
return false; return false;
/* Binary search in sorted positions */
size_t lo = 0;
size_t hi = count - 1;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (positions[mid] < pos)
lo = mid + 1;
else
hi = mid;
} }
return false;
return positions[lo] == pos;
} }
ekp_result_t *ekp_break_with_prefixes( ekp_result_t *ekp_break_with_prefixes(