clear of this danger, but implies that our proofs cannot be quite as simple and perhaps cannot have quite the same structure as their paper counterparts.
A key issue that we run against is the handling of existential quantifiers.
According to what was said earlier, the specification of a sorting algorithm, say mergesort, should be, roughly: “there exists a cost function f ∈O(λn.nlogn) such that mergesort is content with $f(n), where n is the length of the input list.” Therefore, the very first step in a na¨ıve proof of mergesort must be to exhibit a witness forf, that is, a concrete cost function. An appropriate witness might be λn.2nlogn, or λn.nlogn+ 3, who knows? This information is not available up front, at the very beginning of the proof; it becomes available only during the proof, as we examine the code of mergesort, step by step. It is not reasonable to expect the human user to guess such a witness. Instead, it seems desirable todelay the production of the witness and tograduallyconstruct a cost expression as the proof progresses. In the case of a nonrecursive function, such as insertionsort, the cost expression, once fully synthesized, yields the desired witness. In the case of a recursive function, such asmergesort, the cost expression yields the body of a recurrence equation, whose solution is the desired witness.
We make the following contributions:
1. We formalize O as a binary domination relation between functions of type A→Z, where the typeAis chosen by the user. Functions of several variables are covered by instantiatingAwith a product type. We contend that, in order to define what it means fora∈Ato “grow large”, or “tend towards infinity”, the type A must be equipped with a filter [6], that is, a quantifier Ua.P. (Eberl [13] does so as well.) We propose a library of lemmas and tactics that can prove nonnegativeness, monotonicity, and domination assertions (Sect.3).
2. We propose a standard style of writing specifications, in the setting of the CFML program verification framework, so that they integrate asymptotic time complexity claims (Sect.4). We define a predicate,specO, which imposes this style and incorporates a few important technical decisions, such as the fact that every cost function must be nonnegative and nondecreasing.
3. We propose a methodology, supported by a collection of Coq tactics, to prove such specifications (Sect.5). Our tactics, which heavily rely on Coq metavari- ables, help gradually synthesize cost expressions for straight-line code and conditionals, and help construct the recurrence equations involved in the anal- ysis of recursive functions, while delaying their resolution.
4. We present several classic examples of complexity analyses (Sect.6), includ- ing: a simple loop inO(n.2n), nested loops inO(n3) andO(nm), binary search inO(logn), and Union-Find in O(α(n)).
Our code can be found online in the form of two standalone Coq libraries and a self-contained archive [16].
complexityO(1)”, “that code has asymptotic complexityO(n)”, and so on. Yet, these assertions are too informal: they do not have sufficiently precise meaning, and can be easily abused to produce flawed paper proofs.
A striking example appears in Fig.2, which shows how one might “prove”
that a recursive function has complexity O(1), whereas its actual cost is O(n).
The flawed proof exploits the (valid) relationO(1) +O(1) =O(1), which means that a sequence of two constant-time code fragments is itself a constant-time code fragment. The flaw lies in the fact that theO notation hides an existential quantification, which is inadvertently swapped with the universal quantification over the parameter n. Indeed, the claim is that “there exists a constant csuch that, for every n, waste(n) runs in at most c computation steps”. However, the proposed proof by induction establishes a much weaker result, to wit: “for every n, there exists a constantc such that waste(n) runs in at mostc steps”.
This result is certainly true, yet does not entail the claim.
An example of a different nature appears in Fig.3. There, the auxiliary func- tiongtakes two integer argumentsnandmand involves two nested loops, over the intervals [1, n] and [1, m]. Its asymptotic complexity isO(n+nm), which, under the hypothesis that m is large enough, can be simplified to O(nm). The reasoning, thus far, is correct. The flaw lies in our attempt to substitute 0 form
Incorrect claim:The OCaml functionwastehas asymptotic complexityO(1).
let rec waste n =
if n > 0 then waste (n -1) Flawed proof:
Let us prove by induction onnthatwaste(n) costsO(1).
– Casen≤0:waste(n) terminates immediately. Therefore, its cost isO(1).
– Casen >0: A call towaste(n) involves constant-time processing, followed with a call towaste(n−1). By the induction hypothesis, the cost of the recursive call is O(1). We conclude that the cost ofwaste(n) isO(1) +O(1), that is,O(1).
Fig. 2.A flawed proof thatwaste(n) costsO(1), when its actual cost isO(n).
Incorrect claim:The OCaml functionfhas asymptotic complexityO(1).
let g (n, m) = for i = 1 to n do
for j = 1 to m do () done done
let f n = g (n, 0) Flawed proof:
– g(n, m) involvesnminner loop iterations, thus costsO(nm).
– The cost off(n) is the cost ofg(n,0), plusO(1). As the cost ofg(n, m) isO(nm), we find, by substituting 0 form, that the cost ofg(n,0) isO(0). Thus,f(n) isO(1).
Fig. 3.A flawed proof thatf(n) costsO(1), when its actual cost isO(n).
Incorrect claim:The OCaml functionhhas asymptotic complexityO(nm2).
1 let h (m, n) =
2 for i = 0 to m−1 do
3 let p = (if i = 0 then pow2 n else n∗i) in
4 for j = 1 to p do ( ) done
5 done
Flawed proof:
– The body of the outer loop (lines 3-4) has asymptotic costO(ni). Indeed, as soon asi >0 holds, the inner loop performsniconstant-time iterations. The case where i= 0 does not matter in an asymptotic analysis.
– The cost ofh(m, n) is the sum of the costs of the iterations of the outer loop:
m−1
i=0 O(ni) = O n·m−1
i=0 i
= O(nm2).
Fig. 4.A flawed proof thath(m, n) costsO(nm2), when its actual cost isO(2n+nm2).
in the boundO(nm). Because this bound is valid only for sufficiently largem, it does not make sense to substitute a specific value form. In other words, from the fact that “g(n, m) costsO(nm) whennandmare sufficiently large”, onecannot deduce anything about the cost of g(n,0). To repair this proof, one must take a step back and prove that g(n, m) has asymptotic complexity O(n+nm)for sufficiently large n and for everym.This factcan be instantiated withm= 0, allowing one to correctly conclude thatg(n,0) costsO(n). We come back to this example in Sect.3.3.
One last example of tempting yet invalid reasoning appears in Fig.4. We borrow it from Howell [19]. This flawed proof exploits the dubious idea that “the asymptotic cost of a loop is the sum of the asymptotic costs of its iterations”. In more precise terms, the proof relies on the following implication, wheref(m, n, i) represents the true cost of the i-th loop iteration and g(m, n, i) represents an asymptotic bound onf(m, n, i):
f(m, n, i)∈O(g(m, n, i)) ⇒ m−1
i=0 f(m, n, i)∈Om−1
i=0 g(m, n, i) As pointed out by Howell, this implication is in fact invalid. Here,f(m, n,0) is 2n andf(m, n, i) wheni >0 isni, while g(m, n, i) is justni. The left-hand side of the above implication holds, but the right-hand side does not, as 2n+m−1
i=1 ni is O(2n +nm2), not O(nm2). The Summation lemma presented later on in this paper (Lemma8) rules out the problem by adding the requirement that f be a nondecreasing function of the loop index i. We discuss in depth later on (Sect.4.5) why cost functions should and can be monotonic.
The examples that we have presented show that the informal reasoning style of paper proofs, where the O notation is used in a loose manner, is unsound.
One cannot hope, in a formal setting, to faithfully mimic this reasoning style. In this paper, we do assign O specifications to functions, because we believe that
this style is elegant, modular and scalable. However, during the analysis of a function body, we abandon theOnotation. We first synthesize a cost expression for the function body, then check that this expression is indeed dominated by the asymptotic bound that appears in the specification.