Question: I was curious and decided to run this code in python: import time def timeit(function): strt = time.time() for _ in range(100_000_000): function() end = time.time() print(end-strt) @timeit def function1(): return 1 * 1 @timeit def function2(): return 1_000_000_000_000_000_000_000_000_000_000 * 1_000_000_000_000_000_000_000_000_000_000 @timeit def function3(): return 1_000_000_000_000_000_000_000_000_000_000 ** 2  Here are my results: 4.712368965148926 9.684480905532837 11.74640703201294  Why is the third function (squaring the number) slower than the second function (multiplying the number by itself)? What's going on in the computer, as I thought that doing exponents was simply multiplying a number by itself directly anyways?

Answer: Here's the implementation of int.__pow__, abridged to only show code that executes for this operation: /* pow(v, w, x) */ static PyObject * long_pow(PyObject *v, PyObject *w, PyObject *x) { PyLongObject *a, *b, *c; /* a,b,c = v,w,x */ int negativeOutput = 0; /* if x<0 return negative output */ PyLongObject *z = NULL; /* accumulated result */ Py_ssize_t i, j; /* counters */ PyLongObject *temp = NULL; PyLongObject *a2 = NULL; /* may temporarily hold a**2 % c */ PyLongObject *table[EXP_TABLE_LEN]; Py_ssize_t num_table_entries = 0; /* a, b, c = v, w, x */ CHECK_BINOP(v, w); a = (PyLongObject*)v; Py_INCREF(a); b = (PyLongObject*)w; Py_INCREF(b); if (PyLong_Check(x)) { /* Not executed */ } else if (x == Py_None) c = NULL; else { /* Not executed */ } if (Py_SIZE(b) < 0 && c == NULL) { /* Not executed */ } if (c) { /* Not executed */ } /* At this point a, b, and c are guaranteed non-negative UNLESS c is NULL, in which case a may be negative. */ z = (PyLongObject *)PyLong_FromLong(1L); if (z == NULL) ; /* Not executed */ /* Perform a modular reduction, X = X % c, but leave X alone if c * is NULL. */ #define REDUCE(X) \ do { \ if (c != NULL) { \ /* Not executed */ \ } \ } while(0) /* Multiply two values, then reduce the result: result = X*Y % c. If c is NULL, skip the mod. */ #define MULT(X, Y, result) \ do { \ temp = (PyLongObject *)long_mul(X, Y); \ if (temp == NULL) \ goto Error; \ Py_XDECREF(result); \ result = temp; \ temp = NULL; \ REDUCE(result); \ } while(0) i = Py_SIZE(b); digit bi = i ? b->ob_digit[i-1] : 0; digit bit; if (i <= 1 && bi <= 3) { /* aim for minimal overhead */ if (bi >= 2) { MULT(a, a, z); if (bi == 3) { /* Not executed */ } } else if (bi == 1) { /* Not executed */ } /* else bi is 0, and z==1 is correct */ } else if (i <= HUGE_EXP_CUTOFF / PyLong_SHIFT ) { /* Not executed */ } else { /* Not executed */ } if (negativeOutput && (Py_SIZE(z) != 0)) { /* Not executed */ } goto Done; Error: /* Not executed */ Done: for (i = 0; i < num_table_entries; ++i) Py_DECREF(table[i]); Py_DECREF(a); Py_DECREF(b); Py_XDECREF(c); Py_XDECREF(a2); Py_XDECREF(temp); return (PyObject *)z; }  Even abridged like that, there's a lot of code checking various cases and manipulating references, because this function needs to be able to handle so much more than just a single multiplication. Buried in all that, inside the MULT macro, there's a single call to long_mul, the integer multiplication implementation: #define MULT(X, Y, result) \ do { \ temp = (PyLongObject *)long_mul(X, Y); \ ...  When you multiply a number with itself with *, it gets to skip past everything I just posted and go straight to long_mul.