Nice bug. I tried to replicate this and indeed, the time to notice that no match is found is growing very fast with the length of the input.
Using a substring check is a good fix, but I tried to change the regex to fix this and: if instead of an end anchor, you can add an optional non-whitespace character at the end of the pattern, then you only have to check whether the optional part is empty. Testing with very long strings which respectively match and don't match shows that the result is immediate in both cases.
(defparameter *scanner*
(ppcre:create-scanner
'(:sequence
(:register
(:greedy-repetition 1 nil :whitespace-char-class))
(:register
(:greedy-repetition 0 1 :non-whitespace-char-class)))))
(let ((length 40000))
(defparameter *no-match*
(let ((string (make-string length :initial-element #\space)))
(setf (char string (1- (length string))) #\+)
string))
(defparameter *match* (make-string length :initial-element #\space)))
(defun end-white-match (string)
(ppcre:do-scans (ms me rs re *scanner* string)
(when (and ms
(= (aref re 1) (aref rs 1)))
(return (values ms me)))))
(time (end-white-match *match*))
0, 40000
;; Evaluation took:
;; 0.000 seconds of real time
;; 0.000000 seconds of total run time (0.000000 user, 0.000000 system)
;; 100.00% CPU
;; 25,139,832 processor cycles
;; 0 bytes consed
(time (end-white-match *no-match*))
NIL
;; Evaluation took:
;; 0.000 seconds of real time
;; 0.000000 seconds of total run time (0.000000 user, 0.000000 system)
;; 100.00% CPU
;; 11,105,364 processor cycles
;; 0 bytes consed