Repository: benhoyt/goawk
Pull request #100 (2022-02-06 04:07:26)

Add API to reduce allocations when re-executing the same program
As described in https://github.com/benhoyt/goawk/issues/94, for some use cases it'd be good to have a new API to allow reusing the heavy allocations/initialization that `ExecProgram` does to set up the interpreter. You use it like so:

```go
// do this once to parse the program and set up
prog, err := parser.ParseProgram([]byte(`BEGIN {}`), nil)
// handle err
p, err := interp.New(prog)
// handle err

// do this many times
p.ResetVars() // this is optional (call if you want to reset the variables between runs)
status, err := p.Execute(&interp.Config{
	Stdin:   inputReader,
	Output:  ioutil.Discard,
	Environ: []string{},
})
// handle err
```

Benchmarks of the existing `ExecProgram` API against this new one show just how much the overhead is reduced when repeating the execution of a program. For a trivial program, it's 100x (!) as fast when you don't do I/O, and almost 20x as fast when you do. The memory allocations per execute are also vastly reduced.

```
$ go test ./interp -run=^$ -bench=Repeat -benchmem
goos: linux
goarch: amd64
pkg: github.com/benhoyt/goawk/interp
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
BenchmarkRepeatExecProgram-8     	   82119	     13686 ns/op	   12680 B/op	      17 allocs/op
BenchmarkRepeatNew-8             	 9028881	       126.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkRepeatIOExecProgram-8   	   51138	     22887 ns/op	   78488 B/op	      30 allocs/op
BenchmarkRepeatIONew-8           	  976191	      1224 ns/op	     240 B/op	      11 allocs/op
PASS
```

jfesler: I like it. It’s also a lot cleaner looking than my last stab at this, particularly due to breaking those up.
jfesler: Processing 750k records. simple single runs.   (It's what I can bang together before bed)

|version|cpu time, seconds|wall time, seconds|
|----|----|----|
|go-awk 1.7.0 stock | 466.0 | 75.15 |
|go-awk 1.7.0 forked| 169.8 | 25.8 |
|go-awk 1.15.0 stock| 457.7 | 73.7 |
|**go-awk 1.15.0 with reuse** :star:| 35.0 | 10.6 |
| without awk (baseline) | 4.8 | 3.6 |

Your results are looking *much* better than the hack job I did. 
jfesler: And for those watching: I'm running the script once *per record* (my input data is not traditional input; the usual awk methods of record separation don't apply).  
benhoyt: Thanks, @jfesler, those results are very helpful ... and look great for your use case! One more thing: is the fact that interpreter state (values of variables, arrays, and so on) actually what you want for your use case? Or do you want variables to continue from where they left off between each `Execute` call?

We could design the API a bit differently to require a manual `interpreter.Reset()` call between each run. And if you want to keep the values of variables between runs, you don't call `Reset`.
jfesler: Exposing a Reset won’t hurt my case, and may even help cut down a few more allocations.  
benhoyt: Updated: added an explicit `ResetVars` method, so you need to call that if you want to reset variables to their (null) defaults between `Execute`s. It's a little more efficient if you don't need to all it, plus it gives people flexibility if they *do* want to keep the values of variables/arrays from the previous run.
jfesler:  - added .Reset
 - added explicitly cached os.Environ()

I found that by not passing os.Environ myself, 40% of the allocations were coming from go-awk doing os.Environ().  Easy enough to cache and pass that in.

|version|cpu time, seconds|wall time, seconds|
|----|----|----|
|go-awk 1.7.0 stock | 466.0 | 75.15 |
|go-awk 1.7.0 forked| 169.8 | 25.8 |
|go-awk 1.15.0 stock| 457.7 | 73.7 |
|go-awk 1.15.0 with reuse| 35.0 | 10.6 |
|go-awk 1.15.0 with latest PR, without Reset() | 30.6 | 7.9 |
|go-awk 1.15.0 with  latest PR, with Reset | 30.6 | 7.9 |
|go-awk 1.15.0 with  latest PR, with Reset, and cached os.Environ :star: | 18.8 | 6.2 |
| without awk (baseline) | 4.8 | 3.6 |


This is pretty amazing.  466 CPU seconds -> 18.8 CPU seconds.  




benhoyt: Those results are awesome, thanks for posting!

Ah yes, I was setting `config.Environ` to `[]string{}` in the benchmarks. I've just pushed a commit which documents how that fields works a bit more clearly, especially in the context of `New...Execute`.

I think I'm going to merge this now. I won't release a new version just yet, so please let me know here or in a new issue if you find any problems with it.

Patch:
diff --git a/README.md b/README.md
index 81c5955..b2958ba 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 AWK is a fascinating text-processing language, and somehow after reading the delightfully-terse [*The AWK Programming Language*](https://ia802309.us.archive.org/25/items/pdfy-MgN0H1joIoDVoIC7/The_AWK_Programming_Language.pdf) I was inspired to write an interpreter for it in Go. So here it is, feature-complete and tested against "the one true AWK" test suite.
 
-[**Read more about how GoAWK works and performs here.**](https://benhoyt.com/writings/goawk/)
+Read the [**original article about how GoAWK works and performs**](https://benhoyt.com/writings/goawk/), or the follow-up article about how I converted the tree-walking interpreter to a [**bytecode compiler and virtual machine**](https://benhoyt.com/writings/goawk-compiler-vm/).
 
 ## Basic usage
 
@@ -67,6 +67,8 @@ if err != nil {
 // 3:abc
 ```
 
+If you need to repeat execution of the same program on different inputs, you can call [`interp.New`](https://pkg.go.dev/github.com/benhoyt/goawk/interp#New) once, and then call the returned object's `Execute` method as many times as you need.
+
 Read the [documentation](https://pkg.go.dev/github.com/benhoyt/goawk) for more details.
 
 ## Differences from AWK
@@ -81,7 +83,7 @@ Additional features GoAWK has over AWK:
 
 Things AWK has over GoAWK:
 
-* CPU-bound AWK scripts are slightly slower than `awk`, and about twice as slow as `gawk` and `mawk`.
+* CPU-bound AWK scripts are slightly slower than `awk`, somewhat slower than `gawk`, and significantly slower than `mawk`.
 * AWK is written by Brian Kernighan.
 
 ## Stability
diff --git a/benchmarks.txt b/benchmarks.txt
index b6bf5aa..24dd94b 100644
--- a/benchmarks.txt
+++ b/benchmarks.txt
@@ -2,175 +2,195 @@ goos: linux
 goarch: amd64
 pkg: github.com/benhoyt/goawk/interp
 cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
-BenchmarkGlobalVars-8             	 4519076	       256.7 ns/op
-BenchmarkGlobalVars-8             	 4747969	       246.8 ns/op
-BenchmarkGlobalVars-8             	 4865484	       246.8 ns/op
-BenchmarkGlobalVars-8             	 4841377	       246.2 ns/op
-BenchmarkGlobalVars-8             	 4853877	       247.7 ns/op
-BenchmarkLocalVars-8              	 4263662	       277.5 ns/op
-BenchmarkLocalVars-8              	 4249694	       279.7 ns/op
-BenchmarkLocalVars-8              	 4291089	       279.9 ns/op
-BenchmarkLocalVars-8              	 4283533	       281.0 ns/op
-BenchmarkLocalVars-8              	 4231434	       279.2 ns/op
-BenchmarkIncrDecr-8               	 8590526	       136.0 ns/op
-BenchmarkIncrDecr-8               	 8566968	       135.8 ns/op
-BenchmarkIncrDecr-8               	 8783347	       136.6 ns/op
-BenchmarkIncrDecr-8               	 8474395	       137.0 ns/op
-BenchmarkIncrDecr-8               	 8462654	       136.1 ns/op
-BenchmarkSimpleBuiltins-8         	 1696399	       706.1 ns/op
-BenchmarkSimpleBuiltins-8         	 1653139	       700.8 ns/op
-BenchmarkSimpleBuiltins-8         	 1701096	       701.0 ns/op
-BenchmarkSimpleBuiltins-8         	 1694427	       701.9 ns/op
-BenchmarkSimpleBuiltins-8         	 1712929	       702.9 ns/op
-BenchmarkBuiltinMatch-8           	  459066	      2644 ns/op
-BenchmarkBuiltinMatch-8           	  439150	      2774 ns/op
-BenchmarkBuiltinMatch-8           	  416991	      2707 ns/op
-BenchmarkBuiltinMatch-8           	  435924	      2700 ns/op
-BenchmarkBuiltinMatch-8           	  408993	      2644 ns/op
-BenchmarkBuiltinLength-8          	 2762505	       407.5 ns/op
-BenchmarkBuiltinLength-8          	 2987187	       397.3 ns/op
-BenchmarkBuiltinLength-8          	 2997288	       399.0 ns/op
-BenchmarkBuiltinLength-8          	 2956594	       398.2 ns/op
-BenchmarkBuiltinLength-8          	 2981725	       398.7 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1047 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1043 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1044 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1040 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1040 ns/op
-BenchmarkBuiltinSubstr-8          	 1697200	       699.0 ns/op
-BenchmarkBuiltinSubstr-8          	 1702663	       706.4 ns/op
-BenchmarkBuiltinSubstr-8          	 1707933	       704.2 ns/op
-BenchmarkBuiltinSubstr-8          	 1711530	       709.1 ns/op
-BenchmarkBuiltinSubstr-8          	 1706503	       705.4 ns/op
-BenchmarkBuiltinSplitSpace-8      	   31533	     37509 ns/op
-BenchmarkBuiltinSplitSpace-8      	   32073	     37728 ns/op
-BenchmarkBuiltinSplitSpace-8      	   31899	     37759 ns/op
-BenchmarkBuiltinSplitSpace-8      	   31404	     37452 ns/op
-BenchmarkBuiltinSplitSpace-8      	   31963	     37747 ns/op
-BenchmarkBuiltinSplitRegex-8      	   13196	     94773 ns/op
-BenchmarkBuiltinSplitRegex-8      	   10000	    103414 ns/op
-BenchmarkBuiltinSplitRegex-8      	   13561	     90755 ns/op
-BenchmarkBuiltinSplitRegex-8      	   12153	     89234 ns/op
-BenchmarkBuiltinSplitRegex-8      	   12247	     95152 ns/op
-BenchmarkBuiltinSub-8             	   85834	     14362 ns/op
-BenchmarkBuiltinSub-8             	   86558	     14585 ns/op
-BenchmarkBuiltinSub-8             	   80666	     14532 ns/op
-BenchmarkBuiltinSub-8             	   85804	     13909 ns/op
-BenchmarkBuiltinSub-8             	   87928	     14258 ns/op
-BenchmarkBuiltinSubAmpersand-8    	   83926	     14540 ns/op
-BenchmarkBuiltinSubAmpersand-8    	   81198	     14455 ns/op
-BenchmarkBuiltinSubAmpersand-8    	   78084	     14282 ns/op
-BenchmarkBuiltinSubAmpersand-8    	   87748	     14083 ns/op
-BenchmarkBuiltinSubAmpersand-8    	   87499	     14554 ns/op
-BenchmarkBuiltinGsub-8            	   73270	     16522 ns/op
-BenchmarkBuiltinGsub-8            	   66537	     16618 ns/op
-BenchmarkBuiltinGsub-8            	   74211	     17975 ns/op
-BenchmarkBuiltinGsub-8            	   71230	     16671 ns/op
-BenchmarkBuiltinGsub-8            	   72212	     16606 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	   68392	     18389 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	   72516	     16556 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	   66288	     17431 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	   72733	     17505 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	   67262	     16465 ns/op
-BenchmarkBuiltinSprintf-8         	  126668	      9205 ns/op
-BenchmarkBuiltinSprintf-8         	  132367	      9196 ns/op
-BenchmarkBuiltinSprintf-8         	  126148	      9359 ns/op
-BenchmarkBuiltinSprintf-8         	  127767	      9263 ns/op
-BenchmarkBuiltinSprintf-8         	  128535	      9308 ns/op
-BenchmarkRecursiveFunc-8          	   81440	     14789 ns/op
-BenchmarkRecursiveFunc-8          	   80480	     14778 ns/op
-BenchmarkRecursiveFunc-8          	   77740	     14600 ns/op
-BenchmarkRecursiveFunc-8          	   83114	     14669 ns/op
-BenchmarkRecursiveFunc-8          	   77486	     14738 ns/op
-BenchmarkFuncCall-8               	  460926	      2668 ns/op
-BenchmarkFuncCall-8               	  453834	      2654 ns/op
-BenchmarkFuncCall-8               	  456934	      2677 ns/op
-BenchmarkFuncCall-8               	  443046	      2645 ns/op
-BenchmarkFuncCall-8               	  445461	      2678 ns/op
-BenchmarkNativeFunc-8             	  114228	     10540 ns/op
-BenchmarkNativeFunc-8             	  110851	     10568 ns/op
-BenchmarkNativeFunc-8             	  113688	     10557 ns/op
-BenchmarkNativeFunc-8             	  109110	     10560 ns/op
-BenchmarkNativeFunc-8             	  115024	     10506 ns/op
-BenchmarkForLoop-8                	  495014	      2402 ns/op
-BenchmarkForLoop-8                	  494186	      2294 ns/op
-BenchmarkForLoop-8                	  484086	      2291 ns/op
-BenchmarkForLoop-8                	  482073	      2335 ns/op
-BenchmarkForLoop-8                	  451021	      2328 ns/op
-BenchmarkForInLoop-8              	  682208	      1587 ns/op
-BenchmarkForInLoop-8              	  680252	      1627 ns/op
-BenchmarkForInLoop-8              	  666670	      1640 ns/op
-BenchmarkForInLoop-8              	  671462	      1658 ns/op
-BenchmarkForInLoop-8              	  676348	      1655 ns/op
-BenchmarkIfStatement-8            	 8078989	       143.3 ns/op
-BenchmarkIfStatement-8            	 8233875	       143.5 ns/op
-BenchmarkIfStatement-8            	 8284282	       143.2 ns/op
-BenchmarkIfStatement-8            	 8259088	       143.1 ns/op
-BenchmarkIfStatement-8            	 8348576	       143.9 ns/op
-BenchmarkCondExpr-8               	 8029416	       143.7 ns/op
-BenchmarkCondExpr-8               	 8129188	       144.0 ns/op
-BenchmarkCondExpr-8               	 8154441	       144.7 ns/op
-BenchmarkCondExpr-8               	 8285720	       144.4 ns/op
-BenchmarkCondExpr-8               	 8115152	       143.8 ns/op
-BenchmarkSimplePattern-8          	 9459618	       126.8 ns/op
-BenchmarkSimplePattern-8          	 9090544	       125.1 ns/op
-BenchmarkSimplePattern-8          	 9741838	       124.3 ns/op
-BenchmarkSimplePattern-8          	 9879030	       124.9 ns/op
-BenchmarkSimplePattern-8          	 9652201	       124.2 ns/op
-BenchmarkGetField-8               	 2947525	       413.4 ns/op
-BenchmarkGetField-8               	 2851140	       414.0 ns/op
-BenchmarkGetField-8               	 2921932	       413.4 ns/op
-BenchmarkGetField-8               	 2905156	       418.3 ns/op
-BenchmarkGetField-8               	 2969442	       418.7 ns/op
-BenchmarkSetField-8               	 1446286	       818.4 ns/op
-BenchmarkSetField-8               	 1438484	       829.3 ns/op
-BenchmarkSetField-8               	 1427611	       824.5 ns/op
-BenchmarkSetField-8               	 1446331	       835.0 ns/op
-BenchmarkSetField-8               	 1441599	       839.6 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1085 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1044 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1057 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1012 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1044 ns/op
-BenchmarkBinaryOperators-8        	 2940910	       405.4 ns/op
-BenchmarkBinaryOperators-8        	 2938093	       408.8 ns/op
-BenchmarkBinaryOperators-8        	 2917204	       404.8 ns/op
-BenchmarkBinaryOperators-8        	 2972577	       407.8 ns/op
-BenchmarkBinaryOperators-8        	 2912239	       406.1 ns/op
-BenchmarkConcatTwo-8              	12173908	       101.2 ns/op
-BenchmarkConcatTwo-8              	11862904	       101.2 ns/op
-BenchmarkConcatTwo-8              	11726065	       102.7 ns/op
-BenchmarkConcatTwo-8              	11225518	       100.7 ns/op
-BenchmarkConcatTwo-8              	11941936	       102.0 ns/op
-BenchmarkConcatSmall-8            	 3569856	       345.8 ns/op
-BenchmarkConcatSmall-8            	 3661503	       340.4 ns/op
-BenchmarkConcatSmall-8            	 3639264	       350.7 ns/op
-BenchmarkConcatSmall-8            	 3577821	       333.9 ns/op
-BenchmarkConcatSmall-8            	 3261736	       332.4 ns/op
-BenchmarkConcatLarge-8            	    1178	   1093489 ns/op
-BenchmarkConcatLarge-8            	    1171	   1074175 ns/op
-BenchmarkConcatLarge-8            	    1075	   1095072 ns/op
-BenchmarkConcatLarge-8            	    1060	   1083906 ns/op
-BenchmarkConcatLarge-8            	    1029	   1130022 ns/op
-BenchmarkComparisons-8            	 3747915	       319.8 ns/op
-BenchmarkComparisons-8            	 3942769	       297.4 ns/op
-BenchmarkComparisons-8            	 3958566	       298.8 ns/op
-BenchmarkComparisons-8            	 3957408	       297.0 ns/op
-BenchmarkComparisons-8            	 4010358	       297.9 ns/op
-BenchmarkArrayOperations-8        	 2253159	       527.7 ns/op
-BenchmarkArrayOperations-8        	 2264696	       523.0 ns/op
-BenchmarkArrayOperations-8        	 2301885	       528.1 ns/op
-BenchmarkArrayOperations-8        	 2249116	       534.1 ns/op
-BenchmarkArrayOperations-8        	 2270390	       525.0 ns/op
-BenchmarkAssign-8                 	 4951560	       240.3 ns/op
-BenchmarkAssign-8                 	 4859994	       239.6 ns/op
-BenchmarkAssign-8                 	 5001558	       240.4 ns/op
-BenchmarkAssign-8                 	 4916307	       239.7 ns/op
-BenchmarkAssign-8                 	 4962186	       242.2 ns/op
-BenchmarkAugAssign-8              	 1297882	       915.4 ns/op
-BenchmarkAugAssign-8              	 1290826	       915.6 ns/op
-BenchmarkAugAssign-8              	 1287932	       920.6 ns/op
-BenchmarkAugAssign-8              	 1307115	       923.6 ns/op
-BenchmarkAugAssign-8              	 1302104	       922.8 ns/op
+BenchmarkGlobalVars-8             	 4452963	       257.4 ns/op
+BenchmarkGlobalVars-8             	 4802697	       245.5 ns/op
+BenchmarkGlobalVars-8             	 4733904	       245.3 ns/op
+BenchmarkGlobalVars-8             	 4704262	       245.7 ns/op
+BenchmarkGlobalVars-8             	 4808929	       246.9 ns/op
+BenchmarkLocalVars-8              	 4146232	       286.2 ns/op
+BenchmarkLocalVars-8              	 4233672	       298.6 ns/op
+BenchmarkLocalVars-8              	 4075712	       297.0 ns/op
+BenchmarkLocalVars-8              	 4317622	       273.4 ns/op
+BenchmarkLocalVars-8              	 4250389	       273.9 ns/op
+BenchmarkIncrDecr-8               	 8365578	       134.7 ns/op
+BenchmarkIncrDecr-8               	 8429664	       134.6 ns/op
+BenchmarkIncrDecr-8               	 8344886	       135.2 ns/op
+BenchmarkIncrDecr-8               	 8412324	       134.9 ns/op
+BenchmarkIncrDecr-8               	 8510596	       134.7 ns/op
+BenchmarkSimpleBuiltins-8         	 1750566	       682.2 ns/op
+BenchmarkSimpleBuiltins-8         	 1739329	       681.8 ns/op
+BenchmarkSimpleBuiltins-8         	 1739404	       681.2 ns/op
+BenchmarkSimpleBuiltins-8         	 1750266	       680.7 ns/op
+BenchmarkSimpleBuiltins-8         	 1737835	       678.8 ns/op
+BenchmarkBuiltinMatch-8           	  450678	      2610 ns/op
+BenchmarkBuiltinMatch-8           	  435978	      2596 ns/op
+BenchmarkBuiltinMatch-8           	  438296	      2634 ns/op
+BenchmarkBuiltinMatch-8           	  435374	      2642 ns/op
+BenchmarkBuiltinMatch-8           	  414988	      2612 ns/op
+BenchmarkBuiltinLength-8          	 2768156	       403.0 ns/op
+BenchmarkBuiltinLength-8          	 2956089	       396.9 ns/op
+BenchmarkBuiltinLength-8          	 2979657	       396.3 ns/op
+BenchmarkBuiltinLength-8          	 2979358	       395.2 ns/op
+BenchmarkBuiltinLength-8          	 2985384	       395.9 ns/op
+BenchmarkBuiltinIndex-8           	 1213416	       983.4 ns/op
+BenchmarkBuiltinIndex-8           	 1215284	       980.7 ns/op
+BenchmarkBuiltinIndex-8           	 1212115	       981.5 ns/op
+BenchmarkBuiltinIndex-8           	 1216857	       985.3 ns/op
+BenchmarkBuiltinIndex-8           	 1209930	       980.3 ns/op
+BenchmarkBuiltinSubstr-8          	 1694650	       702.2 ns/op
+BenchmarkBuiltinSubstr-8          	 1687875	       703.0 ns/op
+BenchmarkBuiltinSubstr-8          	 1695258	       702.3 ns/op
+BenchmarkBuiltinSubstr-8          	 1685650	       701.7 ns/op
+BenchmarkBuiltinSubstr-8          	 1705531	       698.1 ns/op
+BenchmarkBuiltinSplitSpace-8      	   34117	     34372 ns/op
+BenchmarkBuiltinSplitSpace-8      	   34399	     34549 ns/op
+BenchmarkBuiltinSplitSpace-8      	   34110	     34525 ns/op
+BenchmarkBuiltinSplitSpace-8      	   34106	     34641 ns/op
+BenchmarkBuiltinSplitSpace-8      	   34077	     34635 ns/op
+BenchmarkBuiltinSplitRegex-8      	   13593	     87773 ns/op
+BenchmarkBuiltinSplitRegex-8      	   13542	     89489 ns/op
+BenchmarkBuiltinSplitRegex-8      	   13318	     92802 ns/op
+BenchmarkBuiltinSplitRegex-8      	   13239	     89866 ns/op
+BenchmarkBuiltinSplitRegex-8      	   13107	     92306 ns/op
+BenchmarkBuiltinSub-8             	   78583	     14484 ns/op
+BenchmarkBuiltinSub-8             	   81855	     14185 ns/op
+BenchmarkBuiltinSub-8             	   82501	     14121 ns/op
+BenchmarkBuiltinSub-8             	   83896	     14214 ns/op
+BenchmarkBuiltinSub-8             	   80136	     13918 ns/op
+BenchmarkBuiltinSubAmpersand-8    	   82534	     14688 ns/op
+BenchmarkBuiltinSubAmpersand-8    	   83776	     14018 ns/op
+BenchmarkBuiltinSubAmpersand-8    	   85182	     13828 ns/op
+BenchmarkBuiltinSubAmpersand-8    	   83809	     14175 ns/op
+BenchmarkBuiltinSubAmpersand-8    	   75410	     14150 ns/op
+BenchmarkBuiltinGsub-8            	   70944	     16665 ns/op
+BenchmarkBuiltinGsub-8            	   67568	     16719 ns/op
+BenchmarkBuiltinGsub-8            	   69176	     18280 ns/op
+BenchmarkBuiltinGsub-8            	   70832	     16823 ns/op
+BenchmarkBuiltinGsub-8            	   70408	     16772 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	   66514	     19269 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	   64100	     17215 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	   71086	     16851 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	   66739	     17746 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	   68853	     16997 ns/op
+BenchmarkBuiltinSprintf-8         	  124518	      9143 ns/op
+BenchmarkBuiltinSprintf-8         	  123852	      9174 ns/op
+BenchmarkBuiltinSprintf-8         	  123346	      9108 ns/op
+BenchmarkBuiltinSprintf-8         	  126882	      9078 ns/op
+BenchmarkBuiltinSprintf-8         	  124651	      9131 ns/op
+BenchmarkRecursiveFunc-8          	   81637	     14454 ns/op
+BenchmarkRecursiveFunc-8          	   80616	     14589 ns/op
+BenchmarkRecursiveFunc-8          	   80631	     14487 ns/op
+BenchmarkRecursiveFunc-8          	   79174	     14477 ns/op
+BenchmarkRecursiveFunc-8          	   79083	     14357 ns/op
+BenchmarkFuncCall-8               	  424869	      2653 ns/op
+BenchmarkFuncCall-8               	  425486	      2672 ns/op
+BenchmarkFuncCall-8               	  425474	      2672 ns/op
+BenchmarkFuncCall-8               	  416666	      2638 ns/op
+BenchmarkFuncCall-8               	  425848	      2623 ns/op
+BenchmarkNativeFunc-8             	  107154	     10531 ns/op
+BenchmarkNativeFunc-8             	  109917	     10556 ns/op
+BenchmarkNativeFunc-8             	  103618	     10515 ns/op
+BenchmarkNativeFunc-8             	  109912	     10578 ns/op
+BenchmarkNativeFunc-8             	  108937	     10586 ns/op
+BenchmarkForLoop-8                	  424537	      2422 ns/op
+BenchmarkForLoop-8                	  472154	      2252 ns/op
+BenchmarkForLoop-8                	  529965	      2256 ns/op
+BenchmarkForLoop-8                	  530246	      2265 ns/op
+BenchmarkForLoop-8                	  487770	      2263 ns/op
+BenchmarkForInLoop-8              	  708872	      1547 ns/op
+BenchmarkForInLoop-8              	  763222	      1586 ns/op
+BenchmarkForInLoop-8              	  673869	      1564 ns/op
+BenchmarkForInLoop-8              	  666830	      1633 ns/op
+BenchmarkForInLoop-8              	  676658	      1582 ns/op
+BenchmarkIfStatement-8            	 8058832	       141.7 ns/op
+BenchmarkIfStatement-8            	 8117970	       141.8 ns/op
+BenchmarkIfStatement-8            	 7899210	       142.0 ns/op
+BenchmarkIfStatement-8            	 8120047	       141.4 ns/op
+BenchmarkIfStatement-8            	 8043615	       141.8 ns/op
+BenchmarkCondExpr-8               	 8089394	       142.1 ns/op
+BenchmarkCondExpr-8               	 7925986	       142.9 ns/op
+BenchmarkCondExpr-8               	 7969204	       142.8 ns/op
+BenchmarkCondExpr-8               	 7884762	       143.5 ns/op
+BenchmarkCondExpr-8               	 8029484	       143.0 ns/op
+BenchmarkSimplePattern-8          	 9741051	       127.3 ns/op
+BenchmarkSimplePattern-8          	 9886864	       125.0 ns/op
+BenchmarkSimplePattern-8          	 9662858	       123.9 ns/op
+BenchmarkSimplePattern-8          	 9710998	       122.6 ns/op
+BenchmarkSimplePattern-8          	 9786861	       126.4 ns/op
+BenchmarkGetField-8               	 3013941	       410.5 ns/op
+BenchmarkGetField-8               	 2889720	       414.8 ns/op
+BenchmarkGetField-8               	 2926536	       417.3 ns/op
+BenchmarkGetField-8               	 2986608	       410.8 ns/op
+BenchmarkGetField-8               	 3008008	       417.6 ns/op
+BenchmarkSetField-8               	 1423513	       867.7 ns/op
+BenchmarkSetField-8               	 1433400	       848.7 ns/op
+BenchmarkSetField-8               	 1395082	       846.5 ns/op
+BenchmarkSetField-8               	 1412407	       849.4 ns/op
+BenchmarkSetField-8               	 1400731	       851.6 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1038 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1000 ns/op
+BenchmarkRegexMatch-8             	 1197013	       993.6 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1004 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1001 ns/op
+BenchmarkBinaryOperators-8        	 2916217	       405.8 ns/op
+BenchmarkBinaryOperators-8        	 2876910	       406.8 ns/op
+BenchmarkBinaryOperators-8        	 2919890	       405.2 ns/op
+BenchmarkBinaryOperators-8        	 2873068	       404.9 ns/op
+BenchmarkBinaryOperators-8        	 2940511	       403.9 ns/op
+BenchmarkConcatTwo-8              	11606740	        96.54 ns/op
+BenchmarkConcatTwo-8              	12118945	        95.83 ns/op
+BenchmarkConcatTwo-8              	11858528	        96.55 ns/op
+BenchmarkConcatTwo-8              	11862546	        95.70 ns/op
+BenchmarkConcatTwo-8              	12000715	        96.47 ns/op
+BenchmarkConcatSmall-8            	 3574564	       323.1 ns/op
+BenchmarkConcatSmall-8            	 3448124	       367.8 ns/op
+BenchmarkConcatSmall-8            	 3620126	       334.5 ns/op
+BenchmarkConcatSmall-8            	 3165552	       331.2 ns/op
+BenchmarkConcatSmall-8            	 3571244	       379.3 ns/op
+BenchmarkConcatLarge-8            	     987	   1037337 ns/op
+BenchmarkConcatLarge-8            	     997	   1051560 ns/op
+BenchmarkConcatLarge-8            	    1018	   1060047 ns/op
+BenchmarkConcatLarge-8            	     994	   1056678 ns/op
+BenchmarkConcatLarge-8            	     992	   1059964 ns/op
+BenchmarkComparisons-8            	 3578632	       313.3 ns/op
+BenchmarkComparisons-8            	 3719775	       296.4 ns/op
+BenchmarkComparisons-8            	 3963684	       295.3 ns/op
+BenchmarkComparisons-8            	 3910376	       295.9 ns/op
+BenchmarkComparisons-8            	 3993914	       296.9 ns/op
+BenchmarkArrayOperations-8        	 2308026	       513.5 ns/op
+BenchmarkArrayOperations-8        	 2313090	       513.2 ns/op
+BenchmarkArrayOperations-8        	 2297734	       514.0 ns/op
+BenchmarkArrayOperations-8        	 2294973	       512.5 ns/op
+BenchmarkArrayOperations-8        	 2221844	       512.1 ns/op
+BenchmarkAssign-8                 	 4824020	       238.4 ns/op
+BenchmarkAssign-8                 	 4898485	       238.9 ns/op
+BenchmarkAssign-8                 	 4796115	       237.5 ns/op
+BenchmarkAssign-8                 	 4926769	       239.4 ns/op
+BenchmarkAssign-8                 	 4819693	       240.0 ns/op
+BenchmarkAugAssign-8              	 1302805	       913.2 ns/op
+BenchmarkAugAssign-8              	 1303772	       915.9 ns/op
+BenchmarkAugAssign-8              	 1301092	       918.6 ns/op
+BenchmarkAugAssign-8              	 1304542	       917.5 ns/op
+BenchmarkAugAssign-8              	 1301575	       918.5 ns/op
+BenchmarkRepeatExecProgram-8      	   85944	     14401 ns/op
+BenchmarkRepeatExecProgram-8      	   79640	     14657 ns/op
+BenchmarkRepeatExecProgram-8      	   86300	     14843 ns/op
+BenchmarkRepeatExecProgram-8      	   87559	     14127 ns/op
+BenchmarkRepeatExecProgram-8      	   85340	     13535 ns/op
+BenchmarkRepeatNew-8              	13132840	        85.62 ns/op
+BenchmarkRepeatNew-8              	13617702	        84.06 ns/op
+BenchmarkRepeatNew-8              	13493631	        80.60 ns/op
+BenchmarkRepeatNew-8              	13760954	        80.64 ns/op
+BenchmarkRepeatNew-8              	14000569	        80.70 ns/op
+BenchmarkRepeatIOExecProgram-8    	   48825	     22879 ns/op
+BenchmarkRepeatIOExecProgram-8    	   49992	     22957 ns/op
+BenchmarkRepeatIOExecProgram-8    	   49755	     23000 ns/op
+BenchmarkRepeatIOExecProgram-8    	   49819	     22898 ns/op
+BenchmarkRepeatIOExecProgram-8    	   49650	     23050 ns/op
+BenchmarkRepeatIONew-8            	 1000000	      1148 ns/op
+BenchmarkRepeatIONew-8            	  950356	      1145 ns/op
+BenchmarkRepeatIONew-8            	 1000000	      1144 ns/op
+BenchmarkRepeatIONew-8            	 1000000	      1142 ns/op
+BenchmarkRepeatIONew-8            	 1000000	      1140 ns/op
 PASS
-ok  	github.com/benhoyt/goawk/interp	272.007s
+ok  	github.com/benhoyt/goawk/interp	304.328s
diff --git a/interp/interp.go b/interp/interp.go
index e7e21b7..b89d8c4 100644
--- a/interp/interp.go
+++ b/interp/interp.go
@@ -1,10 +1,14 @@
-// Package interp is the GoAWK interpreter (a simple tree-walker).
+// Package interp is the GoAWK interpreter.
 //
 // For basic usage, use the Exec function. For more complicated use
 // cases and configuration options, first use the parser package to
 // parse the AWK source, and then use ExecProgram to execute it with
 // a specific configuration.
 //
+// If you need to re-run the same parsed program repeatedly on different
+// inputs or with different variables, use New to instantiate an Interpreter
+// and then call the Interpreter.Execute method as many times as you need.
+//
 package interp
 
 import (
@@ -35,6 +39,8 @@ var (
 
 	crlfNewline = runtime.GOOS == "windows"
 	varRegex    = regexp.MustCompile(`^([_a-zA-Z][_a-zA-Z0-9]*)=(.*)`)
+
+	defaultShellCommand = getDefaultShellCommand()
 )
 
 // Error (actually *Error) is returned by Exec and Eval functions on
@@ -69,6 +75,7 @@ type interp struct {
 	filenameIndex int
 	hadFiles      bool
 	input         io.Reader
+	inputBuffer   []byte
 	inputStreams  map[string]io.ReadCloser
 	outputStreams map[string]io.WriteCloser
 	commands      map[string]*exec.Cmd
@@ -146,8 +153,9 @@ type Config struct {
 	// Standard input reader (defaults to os.Stdin)
 	Stdin io.Reader
 
-	// Writer for normal output (defaults to a buffered version of
-	// os.Stdout)
+	// Writer for normal output (defaults to a buffered version of os.Stdout).
+	// If you need to write to stdout but want control over the buffer size or
+	// allocation, wrap os.Stdout yourself and set Output to that.
 	Output io.Writer
 
 	// Writer for non-fatal error messages (defaults to os.Stderr)
@@ -208,6 +216,9 @@ type Config struct {
 	// List of name-value pairs to be assigned to the ENVIRON special
 	// array, for example []string{"USER", "bob", "HOME", "/home/bob"}.
 	// If nil (the default), values from os.Environ() are used.
+	//
+	// If the script doesn't need environment variables, set Environ to a
+	// non-nil empty slice, []string{}.
 	Environ []string
 }
 
@@ -215,14 +226,20 @@ type Config struct {
 // config, returning the exit status code of the program. Error is nil
 // on successful execution of the program, even if the program returns
 // a non-zero status code.
+//
+// As of GoAWK version v1.16.0, a nil config is valid and will use the
+// defaults (zero values). However, it may be simpler to use Exec in that
+// case.
 func ExecProgram(program *parser.Program, config *Config) (int, error) {
-	if len(config.Vars)%2 != 0 {
-		return 0, newError("length of config.Vars must be a multiple of 2, not %d", len(config.Vars))
-	}
-	if len(config.Environ)%2 != 0 {
-		return 0, newError("length of config.Environ must be a multiple of 2, not %d", len(config.Environ))
+	p := newInterp(program)
+	err := p.setExecuteConfig(config)
+	if err != nil {
+		return 0, err
 	}
+	return p.executeAll()
+}
 
+func newInterp(program *parser.Program) *interp {
 	p := &interp{
 		program:   program,
 		functions: program.Compiled.Functions,
@@ -252,16 +269,28 @@ func ExecProgram(program *parser.Program, config *Config) (int, error) {
 	p.outputFieldSep = " "
 	p.outputRecordSep = "\n"
 	p.subscriptSep = "\x1c"
-	p.noExec = config.NoExec
-	p.noFileWrites = config.NoFileWrites
-	p.noFileReads = config.NoFileReads
-	err := p.initNativeFuncs(config.Funcs)
-	if err != nil {
-		return 0, err
+
+	p.inputStreams = make(map[string]io.ReadCloser)
+	p.outputStreams = make(map[string]io.WriteCloser)
+	p.commands = make(map[string]*exec.Cmd)
+	p.scanners = make(map[string]*bufio.Scanner)
+
+	return p
+}
+
+func (p *interp) setExecuteConfig(config *Config) error {
+	if config == nil {
+		config = &Config{}
+	}
+	if len(config.Vars)%2 != 0 {
+		return newError("length of config.Vars must be a multiple of 2, not %d", len(config.Vars))
+	}
+	if len(config.Environ)%2 != 0 {
+		return newError("length of config.Environ must be a multiple of 2, not %d", len(config.Environ))
 	}
 
 	// Setup ARGV and other variables from config
-	argvIndex := program.Arrays["ARGV"]
+	argvIndex := p.program.Arrays["ARGV"]
 	p.setArrayValue(ast.ScopeGlobal, argvIndex, "0", str(config.Argv0))
 	p.argc = len(config.Args) + 1
 	for i, arg := range config.Args {
@@ -272,12 +301,12 @@ func ExecProgram(program *parser.Program, config *Config) (int, error) {
 	for i := 0; i < len(config.Vars); i += 2 {
 		err := p.setVarByName(config.Vars[i], config.Vars[i+1])
 		if err != nil {
-			return 0, err
+			return err
 		}
 	}
 
 	// Setup ENVIRON from config or environment variables
-	environIndex := program.Arrays["ENVIRON"]
+	environIndex := p.program.Arrays["ENVIRON"]
 	if config.Environ != nil {
 		for i := 0; i < len(config.Environ); i += 2 {
 			p.setArrayValue(ast.ScopeGlobal, environIndex, config.Environ[i], numStr(config.Environ[i+1]))
@@ -295,14 +324,13 @@ func ExecProgram(program *parser.Program, config *Config) (int, error) {
 	if len(config.ShellCommand) != 0 {
 		p.shellCommand = config.ShellCommand
 	} else {
-		executable := "/bin/sh"
-		if runtime.GOOS == "windows" {
-			executable = "sh"
-		}
-		p.shellCommand = []string{executable, "-c"}
+		p.shellCommand = defaultShellCommand
 	}
 
 	// Setup I/O structures
+	p.noExec = config.NoExec
+	p.noFileWrites = config.NoFileWrites
+	p.noFileReads = config.NoFileReads
 	p.stdin = config.Stdin
 	if p.stdin == nil {
 		p.stdin = os.Stdin
@@ -315,27 +343,36 @@ func ExecProgram(program *parser.Program, config *Config) (int, error) {
 	if p.errorOutput == nil {
 		p.errorOutput = os.Stderr
 	}
-	p.inputStreams = make(map[string]io.ReadCloser)
-	p.outputStreams = make(map[string]io.WriteCloser)
-	p.commands = make(map[string]*exec.Cmd)
-	p.scanners = make(map[string]*bufio.Scanner)
+
+	// Initialize native Go functions
+	if p.nativeFuncs == nil {
+		err := p.initNativeFuncs(config.Funcs)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (p *interp) executeAll() (int, error) {
 	defer p.closeAll()
 
 	// Execute the program: BEGIN, then pattern/actions, then END
-	err = p.execute(program.Compiled.Begin)
+	err := p.execute(p.program.Compiled.Begin)
 	if err != nil && err != errExit {
 		return 0, err
 	}
-	if program.Actions == nil && program.End == nil {
+	if p.program.Actions == nil && p.program.End == nil {
 		return p.exitStatus, nil
 	}
 	if err != errExit {
-		err = p.execActions(program.Compiled.Actions)
+		err = p.execActions(p.program.Compiled.Actions)
 		if err != nil && err != errExit {
 			return 0, err
 		}
 	}
-	err = p.execute(program.Compiled.End)
+	err = p.execute(p.program.Compiled.End)
 	if err != nil && err != errExit {
 		return 0, err
 	}
@@ -664,3 +701,11 @@ func (p *interp) compileRegex(regex string) (*regexp.Regexp, error) {
 	}
 	return re, nil
 }
+
+func getDefaultShellCommand() []string {
+	executable := "/bin/sh"
+	if runtime.GOOS == "windows" {
+		executable = "sh"
+	}
+	return []string{executable, "-c"}
+}
diff --git a/interp/io.go b/interp/io.go
index 07ebd52..7e9ba9e 100644
--- a/interp/io.go
+++ b/interp/io.go
@@ -135,7 +135,7 @@ func (p *interp) getInputScannerFile(name string) (*bufio.Scanner, error) {
 		if scanner, ok := p.scanners["-"]; ok {
 			return scanner, nil
 		}
-		scanner := p.newScanner(p.stdin)
+		scanner := p.newScanner(p.stdin, make([]byte, inputBufSize))
 		p.scanners[name] = scanner
 		return scanner, nil
 	}
@@ -146,7 +146,7 @@ func (p *interp) getInputScannerFile(name string) (*bufio.Scanner, error) {
 	if err != nil {
 		return nil, err // *os.PathError is handled by caller (getline returns -1)
 	}
-	scanner := p.newScanner(r)
+	scanner := p.newScanner(r, make([]byte, inputBufSize))
 	p.scanners[name] = scanner
 	p.inputStreams[name] = r
 	return scanner, nil
@@ -176,7 +176,7 @@ func (p *interp) getInputScannerPipe(name string) (*bufio.Scanner, error) {
 		p.printErrorf("%s\n", err)
 		return bufio.NewScanner(strings.NewReader("")), nil
 	}
-	scanner := p.newScanner(r)
+	scanner := p.newScanner(r, make([]byte, inputBufSize))
 	p.commands[name] = cmd
 	p.inputStreams[name] = r
 	p.scanners[name] = scanner
@@ -184,7 +184,7 @@ func (p *interp) getInputScannerPipe(name string) (*bufio.Scanner, error) {
 }
 
 // Create a new buffered Scanner for reading input records
-func (p *interp) newScanner(input io.Reader) *bufio.Scanner {
+func (p *interp) newScanner(input io.Reader, buffer []byte) *bufio.Scanner {
 	scanner := bufio.NewScanner(input)
 	switch {
 	case p.recordSep == "\n":
@@ -201,7 +201,6 @@ func (p *interp) newScanner(input io.Reader) *bufio.Scanner {
 		splitter := regexSplitter{p.recordSepRegex, &p.recordTerminator}
 		scanner.Split(splitter.scan)
 	}
-	buffer := make([]byte, inputBufSize)
 	scanner.Buffer(buffer, maxRecordLength)
 	return scanner
 }
@@ -439,7 +438,10 @@ func (p *interp) nextLine() (string, error) {
 					p.hadFiles = true
 				}
 			}
-			p.scanner = p.newScanner(p.input)
+			if p.inputBuffer == nil { // reuse buffer from last input file
+				p.inputBuffer = make([]byte, inputBufSize)
+			}
+			p.scanner = p.newScanner(p.input, p.inputBuffer)
 		}
 		p.recordTerminator = p.recordSep // will be overridden if RS is "" or multiple chars
 		if p.scanner.Scan() {
diff --git a/interp/newexecute.go b/interp/newexecute.go
new file mode 100644
index 0000000..f94f812
--- /dev/null
+++ b/interp/newexecute.go
@@ -0,0 +1,133 @@
+// The New...Execute API (allows you to efficiently execute the same program repeatedly).
+
+package interp
+
+import (
+	"math"
+
+	"github.com/benhoyt/goawk/parser"
+)
+
+// Interpreter is an interpreter for a specific program, allowing you to
+// efficiently execute the same program over and over with different inputs.
+// Use New to create an Interpreter.
+//
+// Most programs won't need reusable execution, and should use the simpler
+// Exec or ExecProgram functions instead.
+type Interpreter struct {
+	interp *interp
+}
+
+// New creates a reusable interpreter for the given program.
+//
+// Most programs won't need reusable execution, and should use the simpler
+// Exec or ExecProgram functions instead.
+func New(program *parser.Program) (*Interpreter, error) {
+	p := newInterp(program)
+	return &Interpreter{interp: p}, nil
+}
+
+// Execute runs this program with the given execution configuration (input,
+// output, and variables) and returns the exit status code of the program. A
+// nil config is valid and will use the defaults (zero values).
+//
+// Internal memory allocations are reused, so calling Execute on the same
+// Interpreter instance is significantly more efficient than calling
+// ExecProgram multiple times.
+//
+// I/O state is reset between each run, but variables and the random number
+// generator seed are not; use ResetVars and ResetRand to reset those.
+//
+// It's best to set config.Environ to a non-nil slice, otherwise Execute will
+// call the relatively inefficient os.Environ each time. Set config.Environ to
+// []string{} if the script doesn't need environment variables, or call
+// os.Environ once and set config.Environ to that value each execution.
+//
+// Note that config.Funcs must be the same value provided to
+// parser.ParseProgram, and must not change between calls to Execute.
+func (p *Interpreter) Execute(config *Config) (int, error) {
+	p.interp.resetCore()
+
+	err := p.interp.setExecuteConfig(config)
+	if err != nil {
+		return 0, err
+	}
+
+	return p.interp.executeAll()
+}
+
+func (p *interp) resetCore() {
+	p.scanner = nil
+	for k := range p.scanners {
+		delete(p.scanners, k)
+	}
+	p.input = nil
+	for k := range p.inputStreams {
+		delete(p.inputStreams, k)
+	}
+	for k := range p.outputStreams {
+		delete(p.outputStreams, k)
+	}
+	for k := range p.commands {
+		delete(p.commands, k)
+	}
+
+	p.sp = 0
+	p.localArrays = p.localArrays[:0]
+	p.callDepth = 0
+
+	p.filename = null()
+	p.line = ""
+	p.lineIsTrueStr = false
+	p.lineNum = 0
+	p.fileLineNum = 0
+	p.fields = nil
+	p.fieldsIsTrueStr = nil
+	p.numFields = 0
+	p.haveFields = false
+
+	p.exitStatus = 0
+}
+
+func (p *interp) resetVars() {
+	// Reset global scalars
+	for i := range p.globals {
+		p.globals[i] = null()
+	}
+
+	// Reset global arrays
+	for _, array := range p.arrays {
+		for k := range array {
+			delete(array, k)
+		}
+	}
+
+	// Reset special variables
+	p.convertFormat = "%.6g"
+	p.outputFormat = "%.6g"
+	p.fieldSep = " "
+	p.fieldSepRegex = nil
+	p.recordSep = "\n"
+	p.recordSepRegex = nil
+	p.recordTerminator = ""
+	p.outputFieldSep = " "
+	p.outputRecordSep = "\n"
+	p.subscriptSep = "\x1c"
+	p.matchLength = 0
+	p.matchStart = 0
+}
+
+// ResetVars resets this interpreter's variables, setting scalar variables to
+// null, clearing arrays, and resetting special variables such as FS and RS to
+// their defaults.
+func (p *Interpreter) ResetVars() {
+	p.interp.resetVars()
+}
+
+// ResetRand resets this interpreter's random number generator seed, so that
+// rand() produces the same sequence it would have after calling New. This is
+// a relatively CPU-intensive operation.
+func (p *Interpreter) ResetRand() {
+	p.interp.randSeed = 1.0
+	p.interp.random.Seed(int64(math.Float64bits(p.interp.randSeed)))
+}
diff --git a/parser/parser.go b/parser/parser.go
index ca69771..1e5ae6b 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1,7 +1,7 @@
 // Package parser is an AWK parser and abstract syntax tree.
 //
-// Use the ParseProgram function to parse an AWK program, and then
-// give the result to one of the interp.Exec* functions to execute it.
+// Use the ParseProgram function to parse an AWK program, and then give the
+// result to interp.Exec, interp.ExecProgram, or interp.New to execute it.
 //
 package parser
 


Test patch:
diff --git a/interp/example_test.go b/interp/example_test.go
index 361c9cb..cd9d484 100644
--- a/interp/example_test.go
+++ b/interp/example_test.go
@@ -94,3 +94,58 @@ func Example_funcs() {
 	// Output:
 	// 0 1 9 xyzxyzxyz
 }
+
+func Example_new() {
+	// We'll execute this program multiple times on different inputs.
+	src := `{ print $1, x, $3; x++ }`
+
+	// Parse the program and set up the interpreter.
+	prog, err := parser.ParseProgram([]byte(src), nil)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	interpreter, err := interp.New(prog)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// Run it once on one input.
+	_, err = interpreter.Execute(&interp.Config{
+		Stdin:   strings.NewReader("one two three"),
+		Environ: []string{}, // avoid calling os.Environ each time
+	})
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// Reset variables and run it again efficiently on a different input (this
+	// could be from a completely different data source).
+	interpreter.ResetVars()
+	_, err = interpreter.Execute(&interp.Config{
+		Stdin:   strings.NewReader("a b c\nd e f\n"),
+		Environ: []string{},
+	})
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// Run it on another input, this time without resetting variables.
+	_, err = interpreter.Execute(&interp.Config{
+		Stdin:   strings.NewReader("x y z"),
+		Environ: []string{},
+	})
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// Output:
+	// one  three
+	// a  c
+	// d 1 f
+	// x 2 z
+}
diff --git a/interp/interp_test.go b/interp/interp_test.go
index 9957418..d8398e4 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -2002,6 +2002,106 @@ BEGIN {
 `, b.N)
 }
 
+func BenchmarkRepeatExecProgram(b *testing.B) {
+	prog, err := parser.ParseProgram([]byte(`BEGIN {}`), nil)
+	if err != nil {
+		b.Fatalf("parse error: %v", err)
+	}
+	config := interp.Config{
+		Output:  ioutil.Discard,
+		Environ: []string{},
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_, err := interp.ExecProgram(prog, &config)
+		if err != nil {
+			b.Fatalf("execute error: %v", err)
+		}
+	}
+}
+
+func BenchmarkRepeatNew(b *testing.B) {
+	prog, err := parser.ParseProgram([]byte(`BEGIN {}`), nil)
+	if err != nil {
+		b.Fatalf("parse error: %v", err)
+	}
+	p, err := interp.New(prog)
+	if err != nil {
+		b.Fatalf("interp.New error: %v", err)
+	}
+	config := interp.Config{
+		Output:  ioutil.Discard,
+		Environ: []string{},
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_, err := p.Execute(&config)
+		if err != nil {
+			b.Fatalf("execute error: %v", err)
+		}
+	}
+}
+
+func BenchmarkRepeatIOExecProgram(b *testing.B) {
+	prog, err := parser.ParseProgram([]byte(`{ for (i=1; i<=NF; i++) print $i }`), nil)
+	if err != nil {
+		b.Fatalf("parse error: %v", err)
+	}
+	inputStr := "foo bar\nbazz\n"
+	input := strings.NewReader(inputStr)
+	var output bytes.Buffer
+	config := interp.Config{
+		Stdin:   input,
+		Output:  &output,
+		Environ: []string{},
+	}
+	expected := "foo\nbar\nbazz\n"
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		input.Reset(inputStr)
+		output.Reset()
+		_, err := interp.ExecProgram(prog, &config)
+		if err != nil {
+			b.Fatalf("execute error: %v", err)
+		}
+		if output.String() != expected {
+			b.Fatalf("expected %q, got %q", expected, output.String())
+		}
+	}
+}
+
+func BenchmarkRepeatIONew(b *testing.B) {
+	prog, err := parser.ParseProgram([]byte(`{ for (i=1; i<=NF; i++) print $i }`), nil)
+	if err != nil {
+		b.Fatalf("parse error: %v", err)
+	}
+	p, err := interp.New(prog)
+	if err != nil {
+		b.Fatalf("interp.New error: %v", err)
+	}
+	inputStr := "foo bar\nbazz\n"
+	input := strings.NewReader(inputStr)
+	var output bytes.Buffer
+	config := interp.Config{
+		Stdin:   input,
+		Output:  &output,
+		Environ: []string{},
+	}
+	expected := "foo\nbar\nbazz\n"
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		input.Reset(inputStr)
+		output.Reset()
+		_, err := p.Execute(&config)
+		if err != nil {
+			b.Fatalf("execute error: %v", err)
+		}
+		if output.String() != expected {
+			b.Fatalf("expected %q, got %q", expected, output.String())
+		}
+	}
+}
+
 func normalizeNewlines(s string) string {
 	return strings.Replace(s, "\r\n", "\n", -1)
 }
diff --git a/interp/newexecute_test.go b/interp/newexecute_test.go
new file mode 100644
index 0000000..c6a3429
--- /dev/null
+++ b/interp/newexecute_test.go
@@ -0,0 +1,123 @@
+// Tests for the New...Execute API.
+
+package interp_test
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/benhoyt/goawk/interp"
+	"github.com/benhoyt/goawk/parser"
+)
+
+// This definitely doesn't test that everything was reset, but it's a good start.
+func TestNewExecute(t *testing.T) {
+	source := `{ print NR, OFMT, x, y, a["k"], $1, $3; OFMT="%g"; x++; y++; a["k"]++ }`
+	program, err := parser.ParseProgram([]byte(source), nil)
+	if err != nil {
+		t.Fatalf("error parsing: %v", err)
+	}
+	interpreter, err := interp.New(program)
+	if err != nil {
+		t.Fatalf("error creating interpreter: %v", err)
+	}
+
+	// First execution.
+	var output bytes.Buffer
+	status, err := interpreter.Execute(&interp.Config{
+		Stdin:  strings.NewReader("one two three\nfour five six\n"),
+		Output: &output,
+	})
+	if err != nil {
+		t.Fatalf("error executing: %v", err)
+	}
+	if status != 0 {
+		t.Fatalf("expected status 0, got %d", status)
+	}
+	normalized := normalizeNewlines(output.String())
+	expected := "1 %.6g    one three\n2 %g 1 1 1 four six\n"
+	if normalized != expected {
+		t.Fatalf("expected %q, got %q", expected, normalized)
+	}
+
+	// Second execution, with ResetVars.
+	output.Reset()
+	interpreter.ResetVars()
+	status, err = interpreter.Execute(&interp.Config{
+		Stdin:  strings.NewReader("ONE TWO THREE\nFOUR FIVE SIX\n"),
+		Output: &output,
+		Vars:   []string{"x", "10"},
+	})
+	if err != nil {
+		t.Fatalf("error executing: %v", err)
+	}
+	if status != 0 {
+		t.Fatalf("expected status 0, got %d", status)
+	}
+	normalized = normalizeNewlines(output.String())
+	expected = "1 %.6g 10   ONE THREE\n2 %g 11 1 1 FOUR SIX\n"
+	if normalized != expected {
+		t.Fatalf("expected %q, got %q", expected, normalized)
+	}
+
+	// Third execution, without ResetVars.
+	output.Reset()
+	status, err = interpreter.Execute(&interp.Config{
+		Stdin:  strings.NewReader("1 2 3\n4 5 6\n"),
+		Output: &output,
+		Vars:   []string{"x", "100"},
+	})
+	if err != nil {
+		t.Fatalf("error executing: %v", err)
+	}
+	if status != 0 {
+		t.Fatalf("expected status 0, got %d", status)
+	}
+	normalized = normalizeNewlines(output.String())
+	expected = "1 %g 100 2 2 1 3\n2 %g 101 3 3 4 6\n"
+	if normalized != expected {
+		t.Fatalf("expected %q, got %q", expected, normalized)
+	}
+}
+
+func TestResetRand(t *testing.T) {
+	source := `BEGIN { print rand(), rand(), rand() }`
+	program, err := parser.ParseProgram([]byte(source), nil)
+	if err != nil {
+		t.Fatalf("error parsing: %v", err)
+	}
+
+	interpreter, err := interp.New(program)
+	if err != nil {
+		t.Fatalf("error creating interpreter: %v", err)
+	}
+	var output bytes.Buffer
+
+	_, err = interpreter.Execute(&interp.Config{Output: &output})
+	if err != nil {
+		t.Fatalf("error executing: %v", err)
+	}
+	original := output.String()
+
+	output.Reset()
+	_, err = interpreter.Execute(&interp.Config{Output: &output})
+	if err != nil {
+		t.Fatalf("error executing: %v", err)
+	}
+	noResetRand := output.String()
+	if original == noResetRand {
+		t.Fatalf("expected different random numbers, got %q both times", original)
+	}
+
+	output.Reset()
+	interpreter.ResetRand()
+	_, err = interpreter.Execute(&interp.Config{Output: &output})
+	if err != nil {
+		t.Fatalf("error executing: %v", err)
+	}
+	withResetRand := output.String()
+	if original != withResetRand {
+		t.Fatalf("expected same random numbers (%q) as original (%q)", withResetRand, original)
+	}
+}