Skip to content

perf(proxy): optimize subdomain checking with incremental string building#113

Open
lamtrinhdev wants to merge 1 commit intoivpn:mainfrom
lamtrinhdev:main
Open

perf(proxy): optimize subdomain checking with incremental string building#113
lamtrinhdev wants to merge 1 commit intoivpn:mainfrom
lamtrinhdev:main

Conversation

@lamtrinhdev
Copy link
Copy Markdown
Contributor

Replace O(n²) strings.Join in loop with O(n) incremental string building. For domains with many subdomains (e.g., a.b.c.d.e.com), this reduces string operations from n*(n+1)/2 to n.

Before: strings.Join(parts[i:], ".") in loop creates n+(n-1)+...+1 operations
After: Build strings incrementally by prepending parts: n operations

PR type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • Documentation content changes
  • Other... Please describe:

PR checklist

Please check if your PR fulfills the following requirements:

  • I have read the CONTRIBUTING.md doc
  • The Git workflow follows our guidelines: CONTRIBUTING.md#git
  • The commit message follows our guidelines: CONTRIBUTING.md#commit
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Other information

Optimizes the subdomain checking logic in proxy/filter/blocklists.go by building candidate domains incrementally instead of using strings.Join in a loop. This reduces the complexity from O(n²) to O(n).

Problem

The previous implementation used strings.Join(parts[i:], ".") inside a loop, which creates O(n²) string operations:

  for i := range len(parts) - 1 {
      candidate := strings.Join(parts[i:], ".")  // O(k) for each iteration
      // ...
  }

For a domain with n parts (e.g., "a.b.c.d.e.com" has 7 parts):

  • Iteration 0: Join 7 parts → 6 string concatenations
  • Iteration 1: Join 6 parts → 5 string concatenations
  • ...
  • Total: n*(n-1)/2 = O(n²) string operations

Build candidate domains incrementally by walking backwards and prepending parts:

  var candidate string
  for i := len(parts) - 1; i >= 0; i-- {
      if i == len(parts)-1 {
          candidate = parts[i]
      } else {
          candidate = parts[i] + "." + candidate  // O(1) string concatenation
      }
      // ...
  }

Before (O(n²)) - Using strings.Join in loop:

  parts := ["a", "b", "c", "com"]  // domain: a.b.c.com

  for i := range len(parts) - 1 {
      candidate := strings.Join(parts[i:], ".")
  }

Let's trace what strings.Join does internally:

i parts[i:] strings.Join operations
0 ["a","b","c","com"] "a" + "." + "b" + "." + "c" + "." + "com" = 3 concatenations
1 ["b","c","com"] "b" + "." + "c" + "." + "com" = 2 concatenations
2 ["c","com"] "c" + "." + "com" = 1 concatenation

Total: 6 concatenations = 3 + 2 + 1 = O(n²)


After (O(n)) - Building incrementally:

  var candidate string
  for i := len(parts) - 1; i >= 0; i-- {
      if i == len(parts)-1 {
          candidate = parts[i]           // No concatenation
      } else {
          candidate = parts[i] + "." + candidate  // 1 concatenation only
      }
  }

Let's trace the operations:

i candidate before Operation candidate after
3 "" candidate = "com" "com" (0 concat)
2 "com" candidate = "c" + "." + "com" "c.com" (1 concat)
1 "c.com" candidate = "b" + "." + "c.com" "b.c.com" (1 concat)
0 "b.c.com" candidate = "a" + "." + "b.c.com" "a.b.c.com" (1 concat)

Total: 3 concatenations = O(n)


Why Does This Matter?

The difference grows exponentially with domain length:

Domain Parts Before (concatenations) After (concatenations) Ratio
4 6 3 2x faster
10 45 9 5x faster
20 190 19 10x faster

For a domain like a.b.c.d.e.f.g.h.i.j.com (11 parts):

  • Before: 55 string concatenations
  • After: 10 string concatenations

…mentally

  Replace O(n²) strings.Join in loop with O(n) incremental string building.
  For domains with many subdomains (e.g., a.b.c.d.e.com), this reduces
  string operations from n*(n+1)/2 to n.

  Before: strings.Join(parts[i:], ".") in loop creates n+(n-1)+...+1 operations
  After: Build strings incrementally by prepending parts: n operations
@lamtrinhdev
Copy link
Copy Markdown
Contributor Author

Dear @MaciejTe and @jurajhilje ,

I have an update for optimize blocklist. When you have time, please help me to review it.

Thanks,
Lam

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant