Home
Man vs. AI: optimizing JavaScript (Claude, Cursor)

How AI beat me at code optimization game.

When I started writing this article I did not expect AI to beat me at optimizing JavaScript code. But it did.
I’m really passionate about optimizing JavaScript. Some say it’s a mental illness but I like my code to go balls to the wall fast. I feel the need. The need for speed.
Optimizing code often requires tedious refactoring.
Can we delegate the tedious parts to AI? Can I just have ideas and get AI to be my programming slave?
Let’s find out.

Optimizing Unicode range lookup with AI

In my experiment I used Cursor with Claude 3.5 Sonnet model. I assume it could be done with other tools / models.
I was browsing pdf.js code and saw this function:
const UnicodeRanges = [
  [0x0000, 0x007f], // 0 - Basic Latin
  ... omited
  [0x0250, 0x02af, 0x1d00, 0x1d7f, 0x1d80, 0x1dbf], // 4 - IPA Extensions - Phonetic Extensions - Phonetic Extensions Supplement
  ... omited
];

function getUnicodeRangeFor(value, lastPosition = -1) {
  // TODO: create a map range => position, sort the ranges and cache it.
  // Then we can make a binary search for finding a range for a given unicode.
  if (lastPosition !== -1) {
    const range = UnicodeRanges[lastPosition];
    for (let i = 0, ii = range.length; i < ii; i += 2) {
      if (value >= range[i] && value <= range[i + 1]) {
        return lastPosition;
      }
    }
  }
  for (let i = 0, ii = UnicodeRanges.length; i < ii; i++) {
    const range = UnicodeRanges[i];
    for (let j = 0, jj = range.length; j < jj; j += 2) {
      if (value >= range[j] && value <= range[j + 1]) {
        return i;
      }
    }
  }
  return -1;
}
This is a simple function: given Unicode character it returns Unicode range (number 0 ... UnicodeRanges.length - 1). The number is cleverly encoded implicitly as array index.
The code already has a TODO about a potential optimization: sort UnicodeRanges and do binary search.
There’s a minor difficulty: some Unicode ranges have multiple intervals, so we have to change the UnicodeRanges to explicitly include the range as 3rd element:
const UnicodeRanges = [
  [0x0000, 0x007f, 0], // 0 - Basic Latin
  [0x0250, 0x02af, 4], // 4 - IPA Extensions - Phonetic Extensions - Phonetic Extensions Supplement
  [0x1d00, 0x1d7f, 4], // 4 - IPA Extensions - Phonetic Extensions - Phonetic Extensions Supplement
  ... omitted
];
Then sort it and change the implementation of getUnicodeRangeFor().
Re-writing UnicodeRanges is simple but tedious and I don’t feel like doing it. Can AI do such change for us?

Changing representation

I’ve selected the whole of UnicodeRanges, added to chat with Ctrl + Shift + L and commanded:
Change representation of UnicodeRanges array where each entry is 3 elements: [start, end, rangeNo]; preserve comments; if rangeNo has multiple ranges, split into separate entries
Cursor responded with:
const UnicodeRanges = [
  [0x0000, 0x007f, 0], // 0 - Basic Latin
  [0x0080, 0x00ff, 1], // 1 - Latin-1 Supplement
  ... omitted
];
There was an issue: it only did part of the array. So I commanded:
don’t be lazy, finish the whole UnicodeRanges array
It did but the result was too long so I had to click Continue button.
It worked and the generated code seems to be correct so I applied it.
For binary search, the array must be sorted, so I commanded:
Sort the array by first element
It did, the result looked correct so I accepted the change.
Hmm, I can get used to having a programming slave.
I copied getUnicodeRangeFor to chat and commanded:
change the implementation to use binary search over UnicodeRanges array
It gave me:
function getUnicodeRangeFor(value, lastPosition = -1) {
  // Check the last position first
  if (lastPosition !== -1) {
    const range = UnicodeRanges[lastPosition];
    if (value >= range[0] && value <= range[1]) {
      return lastPosition;
    }
  }

  // Binary search
  let left = 0;
  let right = UnicodeRanges.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const range = UnicodeRanges[mid];

    if (value < range[0]) {
      right = mid - 1;
    } else if (value > range[1]) {
      left = mid + 1;
    } else {
      return range[2]; // Return the range index
    }
  }

  return -1; // Not found
}
It looks correct so I accepted it.
We achieved the goal: replaced linear search with binary search using cheap AI labor.
But I want more. More speed.

Changing array of arrays to flat array

A more efficient representation of the ranges would be a flat array:
const UnicodeRanges = [
  0x0000, 0x007f, 0, // 0 - Basic Latin
  0x0080, 0x00ff, 1, // 1 - Latin-1 Supplement
];
Let’s see if our AI slave can do it:
change the whole UnicodeRanges array to flat array, preserve comments, don’t be lazy and do all of it
I pre-emptively command the AI to convert the whole array. It did it.
Now is the time to update getUnicodeRangeFor(). It’s a simple change I could do myself: now we just multiply index * 3 to get range definition. But I kind of like commanding the AI so I did it again:
update getUnicodeRangeFor for new flat array representations of UnicodeRanges
It gave me:
function getUnicodeRangeFor(value, lastPosition = -1) {
  // Check the last position first
  if (lastPosition !== -1) {
    const index = lastPosition * 3;
    if (value >= UnicodeRanges[index] && value <= UnicodeRanges[index + 1]) {
      return UnicodeRanges[index + 2];
    }
  }

  // Binary search
  let left = 0;
  let right = UnicodeRanges.length / 3 - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const index = mid * 3;
    const rangeStart = UnicodeRanges[index];
    const rangeEnd = UnicodeRanges[index + 1];

    if (value < rangeStart) {
      right = mid - 1;
    } else if (value > rangeEnd) {
      left = mid + 1;
    } else {
      return UnicodeRanges[index + 2]; // Return the range index
    }
  }

  return -1; // Not found
}
It does seem correct, but let’s do one more tweak:
move binary search code to its own function
And it worked:
function binarySearchUnicodeRange(value) {
  let left = 0;
  let right = UnicodeRanges.length / 3 - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const index = mid * 3;
    const rangeStart = UnicodeRanges[index];
    const rangeEnd = UnicodeRanges[index + 1];

    if (value < rangeStart) {
      right = mid - 1;
    } else if (value > rangeEnd) {
      left = mid + 1;
    } else {
      return UnicodeRanges[index + 2]; // Return the range index
    }
  }

  return -1; // Not found
}

function getUnicodeRangeFor(value, lastPosition = -1) {
  // Check the last position first
  if (lastPosition !== -1) {
    const index = lastPosition * 3;
    if (value >= UnicodeRanges[index] && value <= UnicodeRanges[index + 1]) {
      return UnicodeRanges[index + 2];
    }
  }

  // Perform binary search
  return binarySearchUnicodeRange(value);
}
It’s good but we haven’t yet reached the levels of insanity I’ve hinted at.
UnicodeRanges is a fixed array. We could “unroll” the binary search into code. It would be very painful to do manually. I would have to write custom code generator to ensure it’s correct but maybe AI can do it for us?
unroll first 3 levels of binarySearchUnicodeRange
To my surprise, it worked. AI understood what I wanted and gave me this:
function getUnicodeRangeFor(value, lastPosition = -1) {
  ... omited

  const length = UnicodeRanges.length / 3;
  let mid = Math.floor(length / 2) * 3;
  
  if (value < UnicodeRanges[mid]) {
    mid = Math.floor(mid / 2);
    if (value < UnicodeRanges[mid]) {
      mid = Math.floor(mid / 2);
    } else if (value > UnicodeRanges[mid + 1]) {
      mid = mid + Math.floor(mid / 2);
    } else {
      return UnicodeRanges[mid + 2];
    }
  } else if (value > UnicodeRanges[mid + 1]) {
    mid = mid + Math.floor(mid / 2);
    if (value < UnicodeRanges[mid]) {
      mid = mid - Math.floor(mid / 4);
    } else if (value > UnicodeRanges[mid + 1]) {
      mid = mid + Math.floor(mid / 4);
    } else {
      return UnicodeRanges[mid + 2];
    }
  } else {
    return UnicodeRanges[mid + 2];
  }

  ... omited
}
Can we do even better?
unroll the values of UnicodeRanges[mid] etc. and replace with constants
Holly cow, it worked:
function getUnicodeRangeFor(value, lastPosition = -1) {
  // Check the last position first
  if (lastPosition !== -1) {
    const index = lastPosition * 3;
    if (value >= UnicodeRanges[index] && value <= UnicodeRanges[index + 1]) {
      return UnicodeRanges[index + 2];
    }
  }

  // Unrolled binary search with constants
  if (value < 0x4E00) { // CJK Unified Ideographs start
    if (value < 0x0530) { // Armenian start
      if (value < 0x0250) { // IPA Extensions start
        if (value < 0x0100) { // Latin Extended-A start
          if (value < 0x0080) { // Latin-1 Supplement start
            return 0; // Basic Latin
          }
          return 1; // Latin-1 Supplement
        }
        if (value < 0x0180) {
          return 2; // Latin Extended-A
        }
        return 3; // Latin Extended-B
      }
      if (value < 0x0300) { // Combining Diacritical Marks start
        if (value < 0x02B0) { // Spacing Modifier Letters start
          return 4; // IPA Extensions
        }
        return 5; // Spacing Modifier Letters
      }
      if (value < 0x0370) { // Greek and Coptic start
        return 6; // Combining Diacritical Marks
      }
      if (value < 0x0400) { // Cyrillic start
        return 7; // Greek and Coptic
      }
      return 9; // Cyrillic
    }
    // Continue with binary search for remaining ranges
    let left = 10; // Index of Armenian in UnicodeRanges
    let right = 102; // Index of CJK Unified Ideographs in UnicodeRanges

    while (left <= right) {
      const mid = Math.floor((left + right) / 2) * 3;
      if (value < UnicodeRanges[mid]) {
        right = Math.floor(mid / 3) - 1;
      } else if (value > UnicodeRanges[mid + 1]) {
        left = Math.floor(mid / 3) + 1;
      } else {
        return UnicodeRanges[mid + 2];
      }
    }
  } else {
    // Binary search for ranges after CJK Unified Ideographs
    let left = 103; // Index after CJK Unified Ideographs in UnicodeRanges
    let right = UnicodeRanges.length / 3 - 1;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2) * 3;
      if (value < UnicodeRanges[mid]) {
        right = Math.floor(mid / 3) - 1;
      } else if (value > UnicodeRanges[mid + 1]) {
        left = Math.floor(mid / 3) + 1;
      } else {
        return UnicodeRanges[mid + 2];
      }
    }
  }

  return -1; // Not found
}
Is it correct? I have no idea. Would be easy to write a test that uses UnicodeRanges as source of truth and tests getUnicodeRangeFor() for every possible value.

Can I just ask AI to do it faster?

So far I micro-managed the AI coder. I was telling it exactly what kind of change I wanted and commanded AI until it did what I wanted.
A good boss delegates. Good boss tells the worker the high level goal and leaves implementation detail to the worker.
Let’s start from scratch and command AI thusly:
could you make a faster implementation of getUnicodeRangeFor()? You can change the representation of UnicodeRanges to achieve that goal
Holly cow again. The AI figured out the binary search:
const UnicodeRanges = [
  { start: 0x0000, end: 0x007f, index: 0 }, // Basic Latin
  { start: 0x0080, end: 0x00ff, index: 1 }, // Latin-1 Supplement
  { start: 0x0100, end: 0x017f, index: 2 }, // Latin Extended-A
  // ... (other ranges)
  { start: 0x1f000, end: 0x1f09f, index: 122 }, // Domino Tiles - Mahjong Tiles
].sort((a, b) => a.start - b.start);

function getUnicodeRangeFor(value, lastPosition = -1) {
  if (lastPosition !== -1) {
    const range = UnicodeRanges[lastPosition];
    if (value >= range.start && value <= range.end) {
      return range.index;
    }
  }

  let left = 0;
  let right = UnicodeRanges.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const range = UnicodeRanges[mid];

    if (value >= range.start && value <= range.end) {
      return range.index;
    }

    if (value < range.start) {
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }

  return -1;
}
The object {start, end, index} is less efficient than my flat array representation and it didn’t unroll the binary search but the core idea of binary search is there.
But just for kicks, maybe there’s more:
can you make it even more efficient?
It did something slightly different, but really the same. I’m afraid no bonus this quarter.
Ok, one last try:
but could you go all out and use every possible implementation trick you know to optkmize getUnicodeRangeFor and UnicodeRanges representation?
Fuck me, I have no idea if the generated code is correct but it does seem it would be faster than the non-unrolled binary search I came up with earlier:
// Preprocess UnicodeRanges into a more efficient structure
const BUCKET_SIZE = 256;
const MAX_UNICODE = 0x10FFFF;
const buckets = new Uint16Array(Math.ceil((MAX_UNICODE + 1) / BUCKET_SIZE));
const ranges = [];

UnicodeRanges.forEach((range, index) => {
  for (let i = 0; i < range.length; i += 2) {
    const start = range[i];
    const end = range[i + 1];
    ranges.push({ start, end, index });
    
    const startBucket = start >>> 8;
    const endBucket = end >>> 8;
    for (let j = startBucket; j <= endBucket; j++) {
      buckets[j] |= (1 << (index & 15));
    }
  }
});

ranges.sort((a, b) => a.start - b.start);

// Create a compact representation of ranges
const compactRanges = new Uint32Array(ranges.length * 3);
ranges.forEach((range, i) => {
  compactRanges[i * 3] = range.start;
  compactRanges[i * 3 + 1] = range.end;
  compactRanges[i * 3 + 2] = range.index;
});

function getUnicodeRangeFor(value, lastPosition = -1) {
  if (value > MAX_UNICODE) return -1;
  
  // Check last position first
  if (lastPosition !== -1) {
    const start = compactRanges[lastPosition * 3];
    const end = compactRanges[lastPosition * 3 + 1];
    if (value >= start && value <= end) {
      return compactRanges[lastPosition * 3 + 2];
    }
  }

  // Quick check using buckets
  const bucket = buckets[value >>> 8];
  if (bucket === 0) return -1;

  // Binary search within the relevant ranges
  let left = 0;
  let right = compactRanges.length / 3 - 1;

  while (left <= right) {
    const mid = (left + right) >>> 1;
    const start = compactRanges[mid * 3];
    const end = compactRanges[mid * 3 + 1];

    if (value < start) {
      right = mid - 1;
    } else if (value > end) {
      left = mid + 1;
    } else {
      return compactRanges[mid * 3 + 2];
    }
  }

  return -1;
}
In retrospect, using Uint32Array for UnicodeRanges array was a no-brainer. It’s more compact in memory, the values will probably be compared as integers, not floats. Should be faster to scan than array of floats.
The buckets are not necessary. They seems to be for quick “doesn’t exist check” which is not important. I believe most lookups are for values that exist.
I’m humbled that just asking for super duper optimization made AI produce something I didn’t think of.

More optimization ideas

I can’t help myself. Those are ideas I didn’t ask AI to implement.
UnicodeRanges is small. A linear search of compact Uint32Array representation where we just have (start, end) values for each range would be faster than binary search due to cache lines.
We could start the search in the middle of array and scan half the data going forward or backwards.
We could also store ranges smaller than 0x10000 in Uint16Array and larger in Uint32Array. And do linear search starting in the middle.
Since the values are smaller than 256, we could encode the first 0xffff values in 64kB as Uint8Array and the rest as Uint32Array.
That would probably be the fastest on average, because I believe most lookups are for Unicode chars smaller than 0xffff.
Finally, we could calculate the the frequency of each range in representative sample of PDF documents, check the ranges based on that frequency, fully unrolled into code, without any tables.

Conclusions

AI is a promising way to do tedious code refactoring.
If I didn’t have the AI, I would have to write a program to e.g. convert UnicodeRanges to a flat representation. It’s simple and therefore doable but certainly would take longer than commanding AI.
The unrolling of getUnicodeRangeFor() would probably never happen. It would require writing a sophisticated code generator which would be a big project by itself.
AI can generate buggy code so it needs to be carefully reviewed.
The unrolled binary search could not be verified by review, it would need a test. But hey, I could command my AI sidekick to write the test for me.
There was this idea of organizing programming teams into master programmer and coding grunts. The job of master programmer was to generate high level ideas and supervising coding grunts that did the actual implementation work.
Turns out that we can’t organize people that way but now we can use AI as our coding grunts.
Prompt engineering is a thing. I wasted a bunch of time doing incremental improvements. I should have started by asking for super-duper optimization.
Productivity gains is real. The whole thing took me about an hour. For this particular task easily 2x compared to not using cheap AI labor. Imagine you’re running a software business and instead of spending 2 months on a task, you only spend 1 month.
I’ll be using more AI for coding.
programming ai
Aug 29 2024

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you: