Run it
Best-of-five timings, because these are noisy, and then the per-iteration bytecode of each form so the cause is visible rather than asserted.
Best of five runs, because a single measurement here is a coin flip.
And the reason, which is visible rather than asserted.
And the version that stores nothing - note what it does and does not buy.
The difference, in bytecode
The loop performs an append call for each value. In CPython, the comprehension uses a specialised list-building path. The dis output above lets you inspect the exact bytecode for the interpreter running this page; those details can change between versions.
Why the number is smaller than people expect
Folklore puts comprehensions at two or three times faster. And that has not been true for a while. Two things narrowed it.
The specialising interpreter added in 3.11 rewrites hot instruction sequences in place. And a repeated attribute-lookup-then-call on the same type is exactly the pattern it specialises. So the loop's overhead shrank.
And in 3.12 comprehensions were inlined (PEP 709). Before that, a list comprehension created and called a hidden function object on every evaluation. Real overhead that partly cancelled the LIST_APPEND saving. Removing it made comprehensions faster in a way that has nothing to do with the append path.
So the answer to give is "a bit faster because of a per-iteration lookup and call", not a multiplier. The editor below measures it on whichever interpreter you are running.
When the loop is the right answer anyway
A comprehension has to be one expression. The moment the body needs a statement — a try, a break, a log line, two things per item — the loop is the only option, and forcing a comprehension produces the unreadable nested version everybody has met.
Choose for readability. One transformation and one filter often reads well as a comprehension; deeply nested conditions are usually clearer in a loop. And if the result is only going to be consumed once, the better answer is often neither. A generator expression skips building the list at all. That is a memory win rather than a speed one — the editor below measures the two as roughly equal in time — but constant memory against a list of a million items is a larger practical difference than the constant factor this page is about.
The follow-up: what about map and filter
map performs iteration in C but still calls its function once per item. A built-in function such as str can make it competitive; a Python lambda still incurs a Python call per value. Compare both forms on the actual workload.
Edge cases to raise
Volunteering these is most of what separates a correct answer from a good one.
The measurement is noisy. The same code timed twice can differ by half. This is why the editor takes the best of five runs - and why quoting a single-run figure in an interview is a claim you cannot defend.
A comprehension over a huge input still builds the whole list. The speed argument is per item; the memory cost is the whole thing. And for a single pass a generator expression removes it.
Readability wins at the point where you have to re-read it. Two for clauses and a condition is usually the limit, and volunteering that boundary is a better answer than defending comprehensions broadly.