{"id":485,"date":"2024-09-10T19:27:37","date_gmt":"2024-09-10T19:27:37","guid":{"rendered":"https:\/\/algocademy.com\/blog\/?p=485"},"modified":"2026-10-15T10:34:50","modified_gmt":"2026-10-15T10:34:50","slug":"10-code-optimization-techniques-to-improve-your-algorithm-efficiency","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/10-code-optimization-techniques-to-improve-your-algorithm-efficiency\/","title":{"rendered":"10 Code Optimization Techniques to Improve Algorithm Efficiency"},"content":{"rendered":"<p>The biggest speedups almost always come from three things, in this order: measuring to find the real bottleneck, picking a better algorithm or data structure, and avoiding repeated work (caching, fewer I\/O calls). Micro-tweaks like swapping a multiply for a shift rarely matter. The ten techniques below are ordered roughly by how much they usually buy you. Examples are in Python, and every output shown was produced by running the code.<\/p>\n<h2>1. Measure before you change anything<\/h2>\n<p>Guessing where code is slow is wrong surprisingly often. Profile first, change one thing, then profile again.<\/p>\n<ul>\n<li><strong>Python:<\/strong> <code>cProfile<\/code> shows time per function, and <code>timeit<\/code> times small snippets.<\/li>\n<li><strong>JavaScript in the browser:<\/strong> the Performance tab in Chrome DevTools.<\/li>\n<li><strong>Anything:<\/strong> a simple timer around the suspicious section, run several times on realistic data.<\/li>\n<\/ul>\n<pre><code class=\"language-python\">import cProfile\n\ncProfile.run(\"main()\", sort=\"cumulative\")   # replace main() with your entry point\n<\/code><\/pre>\n<p>Donald Knuth&#8217;s often-quoted line is &#8220;premature optimization is the root of all evil&#8221;. The full sentence is about small efficiencies in non-critical parts of a program: you should not spend effort on them, but you should not pass up the critical few percent either. Profiling is how you find that few percent.<\/p>\n<h2>2. Choose a better algorithm<\/h2>\n<p>This is where order-of-magnitude gains live. If the input doubles and your runtime quadruples, you probably have a nested loop that a different approach can remove. Compare two ways to check for duplicates:<\/p>\n<pre><code class=\"language-python\">def dup_quadratic(nums):\n    for i in range(len(nums)):\n        for j in range(i + 1, len(nums)):\n            if nums[i] == nums[j]:\n                return True\n    return False\n\ndef dup_set(nums):\n    seen = set()\n    for x in nums:\n        if x in seen:\n            return True\n        seen.add(x)\n    return False\n\nprint(dup_quadratic([1, 2, 3, 2]), dup_set([1, 2, 3, 2]))   # True True\ndata = list(range(3000))\nprint(dup_quadratic(data), dup_set(data))                   # False False\n<\/code><\/pre>\n<p>Both give the same answers. The first is O(n^2) and does about 4.5 million comparisons on the 3,000 items above; the second is O(n) on average. In a quick timing run the quadratic version was more than 20 times slower on that input, and the gap keeps growing with size. The rule of thumb: sorting a large input needs an O(n log n) algorithm, not bubble sort, and any &#8220;for each item, scan the whole list&#8221; pattern is a hint to use a hash map or sort first. If you need a refresher on the notation, see <a href=\"https:\/\/algocademy.com\/blog\/understanding-algorithms-time-complexity-explained\/\">time complexity explained<\/a>.<\/p>\n<h2>3. Pick the right data structure<\/h2>\n<p>The same idea applies to the container you choose:<\/p>\n<ul>\n<li>Membership tests (<code>x in collection<\/code>): a <code>list<\/code> scans every element (O(n)); a <code>set<\/code> or <code>dict<\/code> is O(1) on average. In a timing test with 100,000 numbers, checking for the last element in the set was over 50 times faster than in the list.<\/li>\n<li>Take items from the front: use <code>collections.deque<\/code>; <code>list.pop(0)<\/code> is O(n).<\/li>\n<li>Always need the smallest item: a heap (<code>heapq<\/code>) instead of re-sorting.<\/li>\n<li>Lookups by key: a dictionary instead of searching a list of pairs.<\/li>\n<\/ul>\n<p>The <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=hash-map&amp;solution=1\">Hash Map lesson<\/a> practices exactly this trade.<\/p>\n<h2>4. Cache repeated work<\/h2>\n<p>If a function is pure (the same input always gives the same output) and called repeatedly with the same arguments, store the result. In Python, <code>functools.lru_cache<\/code> does it in one line:<\/p>\n<pre><code class=\"language-python\">from functools import lru_cache\n\ncalls = 0\ndef fib(n):\n    global calls\n    calls += 1\n    return n if n &lt; 2 else fib(n - 1) + fib(n - 2)\n\nfib(25)\nprint(\"plain calls:\", calls)          # plain calls: 242785\n\n@lru_cache(maxsize=None)\ndef fib_cached(n):\n    return n if n &lt; 2 else fib_cached(n - 1) + fib_cached(n - 2)\n\nfib_cached(25)\nprint(fib_cached.cache_info())        # CacheInfo(hits=23, misses=26, maxsize=None, currsize=26)\n<\/code><\/pre>\n<p>242,785 calls become 26 real computations. This is memoization, the core idea of top-down dynamic programming; see <a href=\"https:\/\/algocademy.com\/blog\/dynamic-programming-mastering-tabulation-and-memoization\/\">tabulation and memoization<\/a>. In applications the same idea appears as Redis or Memcached for shared caches. The hard parts are not the lookup but the rules: how long entries live, when to evict them, and how to invalidate them when the underlying data changes. A cache that serves stale data is a bug. Also keep an eye on memory, since an unbounded cache is a leak (use <code>maxsize<\/code> when keys are many).<\/p>\n<h2>5. Do less input and output<\/h2>\n<p>Reading files, querying databases and calling APIs are usually orders of magnitude slower than in-memory work. Reduce the number of round trips:<\/p>\n<ul>\n<li><strong>Batch:<\/strong> one query that fetches 100 rows beats 100 queries that fetch one. This is the classic &#8220;N+1 query&#8221; problem in database code.<\/li>\n<li><strong>Buffer:<\/strong> write in larger chunks rather than one tiny write per line.<\/li>\n<li><strong>Read what you need:<\/strong> select specific columns, paginate, and stream large files line by line.<\/li>\n<li><strong>Overlap waiting:<\/strong> async I\/O or threads let you wait on several network calls at once.<\/li>\n<\/ul>\n<h2>6. Use less memory: generators and streaming<\/h2>\n<p>Do not build a million-item list if you only need to loop over it once. A generator produces items on demand:<\/p>\n<pre><code class=\"language-python\">import sys\n\nbig_list = [i * i for i in range(1_000_000)]\ngen = (i * i for i in range(1_000_000))\nprint(sys.getsizeof(big_list) &gt; 1_000_000)   # True: megabytes just for the list's pointers\nprint(sys.getsizeof(gen) &lt; 500)              # True: the generator object is tiny\nprint(sum(i * i for i in range(1_000_000)) == sum(big_list))   # True\n<\/code><\/pre>\n<p>The generator holds only its current state. Other memory savers: iterate over files line by line, avoid copying large lists (slicing copies), and use sets when you only need uniqueness. Note that setting a variable to <code>None<\/code> rarely helps in garbage-collected languages; the memory is reclaimed once nothing references it, so the real fix is usually a narrower scope or streaming.<\/p>\n<h2>7. Vectorize numeric work<\/h2>\n<p>In Python, a loop over a million numbers runs interpreter code a million times. NumPy performs the loop in compiled code:<\/p>\n<pre><code class=\"language-python\">import numpy as np\n\na = np.arange(1_000_000, dtype=np.int64)\nloop = sum(int(x) * 2 for x in a)\nvec = int((a * 2).sum())\nprint(loop == vec, vec)   # True 999999000000\n<\/code><\/pre>\n<p>Same result; the vectorized line was more than 10 times faster in a quick timing run. The same idea applies in pandas (avoid row-by-row <code>apply<\/code> when a column operation exists) and in SQL (let the database aggregate instead of pulling rows into your code). Vectorizing makes the code shorter too, but check that the dtype can hold your values to avoid overflow.<\/p>\n<h2>8. Build strings and collections efficiently<\/h2>\n<p>Many small repeated operations hide quadratic costs. In languages where strings are immutable, concatenating in a loop can copy the whole string every time. Collect the parts and join once:<\/p>\n<pre><code class=\"language-python\">parts = [str(i) for i in range(1000)]\ns = \"\".join(parts)       # one pass, instead of s += p in a loop\n<\/code><\/pre>\n<p>CPython sometimes optimizes <code>+=<\/code> on strings, but that is an implementation detail you should not rely on. Similar traps: calling <code>len()<\/code> or a database lookup inside a loop condition, recomputing the same expression inside a loop (move it out), and repeatedly slicing or sorting inside a loop.<\/p>\n<h2>9. Use parallelism for the right kind of work<\/h2>\n<p>Parallelism helps when you have many independent tasks. Match the tool to the kind of work:<\/p>\n<ul>\n<li><strong>Waiting on the network or disk:<\/strong> threads or <code>asyncio<\/code> in Python; promises in JavaScript.<\/li>\n<li><strong>Heavy CPU work in Python:<\/strong> use processes (<code>multiprocessing<\/code> or <code>concurrent.futures.ProcessPoolExecutor<\/code>), because of the Global Interpreter Lock in standard CPython threads. Recent Python versions also offer an optional free-threaded build; check the docs for your version.<\/li>\n<li><strong>C, C++, Java, Go, Rust:<\/strong> real threads, or libraries such as OpenMP for loops.<\/li>\n<\/ul>\n<p>It also adds risks: race conditions, deadlocks, and overhead that can make small tasks slower. Parallelism cannot rescue a bad algorithm, so apply it after sections 2 to 5, and measure.<\/p>\n<h2>10. Let the compiler and runtime help, and know what not to micro-optimize<\/h2>\n<p>Compilers for C, C++, Rust, Go and Java&#8217;s JIT already do constant folding, dead-code elimination, inlining and loop optimizations. Turn them on (for example <code>-O2<\/code> on GCC or Clang for release builds) and write clear code instead of hand-tuning. Folklore like &#8220;shifts are faster than multiplies&#8221; is usually already handled by the compiler, and the small gains are not worth the loss in readability.<\/p>\n<p>On the same note, &#8220;avoid global variables&#8221; is good design advice, because globals make code harder to test and reason about. Their performance cost in Python is real but small: a global is looked up in a dictionary, a local in a fast slot. Fix globals for clarity; do not expect a big speedup from it.<\/p>\n<p>For web pages specifically, lazy loading defers work until needed: add <code>loading=\"lazy\"<\/code> to below-the-fold images and load non-critical scripts with <code>defer<\/code> or <code>async<\/code>.<\/p>\n<h2>A simple workflow<\/h2>\n<ol>\n<li>Write the clearest correct version first. Add tests.<\/li>\n<li>If it is too slow, profile with realistic data.<\/li>\n<li>Fix the top item: algorithm, data structure, repeated work, I\/O. Re-measure.<\/li>\n<li>Stop when it meets your target. Further tuning costs readability.<\/li>\n<\/ol>\n<p>Choosing algorithms well is also what coding interviews test; the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=time-complexity&amp;solution=0\">Big O lessons<\/a> and the hash-table exercises are a good place to practice it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Ten practical ways to make code faster and lighter, in order of payoff: measure first, fix the algorithm, cache, cut I\/O, use generators and vectorization.<\/p>\n","protected":false},"author":1,"featured_media":549,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-485","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/485"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=485"}],"version-history":[{"count":3,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/485\/revisions"}],"predecessor-version":[{"id":8758,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/485\/revisions\/8758"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/549"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=485"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=485"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=485"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}