{"action":"created","issue":{"url":"https://api.github.com/repos/rails/rails/issues/24658","repository_url":"https://api.github.com/repos/rails/rails","labels_url":"https://api.github.com/repos/rails/rails/issues/24658/labels{/name}","comments_url":"https://api.github.com/repos/rails/rails/issues/24658/comments","events_url":"https://api.github.com/repos/rails/rails/issues/24658/events","html_url":"https://github.com/rails/rails/pull/24658","id":149880557,"number":24658,"title":"Speed up String#blank? Regex","user":{"login":"schneems","id":59744,"avatar_url":"https://avatars.githubusercontent.com/u/59744?v=3","gravatar_id":"","url":"https://api.github.com/users/schneems","html_url":"https://github.com/schneems","followers_url":"https://api.github.com/users/schneems/followers","following_url":"https://api.github.com/users/schneems/following{/other_user}","gists_url":"https://api.github.com/users/schneems/gists{/gist_id}","starred_url":"https://api.github.com/users/schneems/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/schneems/subscriptions","organizations_url":"https://api.github.com/users/schneems/orgs","repos_url":"https://api.github.com/users/schneems/repos","events_url":"https://api.github.com/users/schneems/events{/privacy}","received_events_url":"https://api.github.com/users/schneems/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/rails/rails/labels/activesupport","name":"activesupport","color":"FC9300"}],"state":"closed","locked":false,"assignee":null,"milestone":null,"comments":11,"created_at":"2016-04-20T20:40:57Z","updated_at":"2016-04-20T23:07:49Z","closed_at":"2016-04-20T21:30:44Z","pull_request":{"url":"https://api.github.com/repos/rails/rails/pulls/24658","html_url":"https://github.com/rails/rails/pull/24658","diff_url":"https://github.com/rails/rails/pull/24658.diff","patch_url":"https://github.com/rails/rails/pull/24658.patch"},"body":"Follow up on https://github.com/rails/rails/commit/697384df36a939e565b7c08725017d49dc83fe40#commitcomment-17184696.\r\n\r\nThe regex to detect a blank string `/\\A[[:space:]]*\\z/` will loop through every character in the string to ensure that all of them are a `:space:` type. We can invert this logic and instead look for any non-`:space:` characters. When that happens, we would return on the first character found and the regex engine does not need to keep looking.\r\n\r\nThanks @nellshamrell for the regex talk at LSRC.\r\n\r\nBy defining a \"blank\" string as any string that does not have a non-whitespace character (yes, double negative) we can get a substantial speed bump.\r\n\r\nAlso an inline regex is (barely) faster than a regex in a constant, since it skips the constant lookup. A regex literal is frozen by default.\r\n\r\n```ruby\r\nrequire 'benchmark/ips'\r\n\r\ndef string_generate\r\n  str = \" abcdefghijklmnopqrstuvwxyz\\t\".freeze\r\n  str[rand(0..(str.length - 1))] * rand(0..23)\r\nend\r\n\r\nstrings = 100.times.map { string_generate }\r\n\r\nALL_WHITESPACE_STAR = /\\A[[:space:]]*\\z/\r\n\r\nBenchmark.ips do |x|\r\n  x.report('current regex            ') { strings.each {|str| str.empty? || ALL_WHITESPACE_STAR === str } }\r\n  x.report('not a non-whitespace char') { strings.each {|str| str.empty? || !(/[[:^space:]]/ === str) } }\r\n  x.compare!\r\nend\r\n\r\n# Warming up --------------------------------------\r\n# current regex\r\n#                          1.744k i/100ms\r\n# not a non-whitespace char\r\n#                          2.264k i/100ms\r\n# Calculating -------------------------------------\r\n# current regex\r\n#                          18.078k (± 8.9%) i/s -     90.688k\r\n# not a non-whitespace char\r\n#                          23.580k (± 7.1%) i/s -    117.728k\r\n\r\n# Comparison:\r\n# not a non-whitespace char:    23580.3 i/s\r\n# current regex            :    18078.2 i/s - 1.30x slower\r\n```\r\n\r\nThis makes the method roughly 30% faster `(23.580 - 18.078)/18.078 * 100`.\r\n\r\ncc/ @fxn"},"comment":{"url":"https://api.github.com/repos/rails/rails/issues/comments/212646266","html_url":"https://github.com/rails/rails/pull/24658#issuecomment-212646266","issue_url":"https://api.github.com/repos/rails/rails/issues/24658","id":212646266,"user":{"login":"fxn","id":3387,"avatar_url":"https://avatars.githubusercontent.com/u/3387?v=3","gravatar_id":"","url":"https://api.github.com/users/fxn","html_url":"https://github.com/fxn","followers_url":"https://api.github.com/users/fxn/followers","following_url":"https://api.github.com/users/fxn/following{/other_user}","gists_url":"https://api.github.com/users/fxn/gists{/gist_id}","starred_url":"https://api.github.com/users/fxn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fxn/subscriptions","organizations_url":"https://api.github.com/users/fxn/orgs","repos_url":"https://api.github.com/users/fxn/repos","events_url":"https://api.github.com/users/fxn/events{/privacy}","received_events_url":"https://api.github.com/users/fxn/received_events","type":"User","site_admin":false},"created_at":"2016-04-20T23:07:49Z","updated_at":"2016-04-20T23:07:49Z","body":"@tenderlove AFAIK it still goes left-to-right so you can fail fast. That is, the engine does not blindly go to the far right, but just matches as much as it can (as long as characters match).\r\n\r\nSo, when the quantifier is greedy you first eat as much as possible *matching*, and then check if the rest of the regexp matches. If not, backtrack. When the quantifier is not greedy, you match as less as possible, and if the rest matches done, otherwise advance.\r\n\r\nIn theory, though, if you have a bunch of whitespace and there is a non-space character, the engine reaches it and then backtracks only to fail, because there's no way to match `\\z` in such situation. Perl's for example does backtrack, as this example shows (in Perl regexps `(?{ ... })` executes Perl code when the engine reaches that point:\r\n\r\n```\r\nfxn@yeager:~ $ perl -le '\"    foo\" =~ /\\A[[:space:]]*(?{print length $&})\\z/'\r\n4\r\n3\r\n2\r\n1\r\n0\r\n```\r\n\r\nThat would certainly explain the difference in performance, since the vanilla character class has no backtracking going on."}}