Skip to content

性能基准 ​

下列数字来自库内 bench_test.go 的 go test -bench,衡量的是写出器 / 模板 / 流式写入成本,不是 Microsoft Word 的打开速度。流式提取用 runtime.MemStats 占用程序展示:额外堆相对文件大小保持 O(1),单次 ns/op 会把这一点淹没。

相关:O(1) 流式提取器、企业级实战案例。

环境 ​

项值
CPUIntel Core i7-10870H @ 2.20 GHz(Go 报告 16 线程)
OSWindows amd64
Go1.21+(模块行)
命令go test -bench="BenchmarkSaveDocx|BenchmarkTableRender|BenchmarkTemplateProcess|BenchmarkStreamWriter" -benchtime=30x -count=1

写入容量规划前请在目标硬件上重跑。分配次数是较稳的信号;墙钟时间随磁盘与 CPU 波动。

写出器 / 模板结果 ​

基准工作负载ns/opB/opallocs/op
BenchmarkSaveDocx30 段 + 20×5 表,WriteTo1.88e6163 KiB949
BenchmarkTableRender50×8 表,WriteTo2.76e6180 KiB1820
BenchmarkTemplateProcessNewTemplateProcessorBytes 做两处 ${} 替换1.43e6238 KiB703
BenchmarkStreamWriter增量段落写入 ZIP1.55e6142 KiB627

B/op 是每次迭代的额外堆,不是 .docx 体积。随着段落增加,流式写出器的分配保持平稳,因为 document.xml 边写边刷。

DOCX → HTML 渲染 ​

HTML 渲染器使用 2,000 段正文和十张五列表格的大文档进行基准测试:

sh
go test . -run '^$' -bench BenchmarkRenderHTMLLargeDocument -benchmem -count=1

测试套件同时检查标签闭合、void 元素、转义和 500 段文档。基准只测 DOM 到 HTML 的渲染,不包含 DOCX 读取时间。

流式解析与内存占用 ​

StreamExtractText / StreamExtractImages 用 encoding/xml.Decoder 扫描 ZIP。每个段落字符串交给回调后即丢弃。额外堆不会像 Load / LoadBytes 那样随页数增长。

API构建 DOM相对文件大小的额外内存
Load / LoadBytes是整棵树
StreamExtractText / StreamExtractImages否O(1)
NewStreamWriter增量写出正文 XML 不会整段缓冲

优先传入 *os.File,这样 archive/zip 无需拷贝即可映射。普通 io.Reader 会先被缓冲。第二次遍历前请回绕或重新打开。

完整示例 — 流式提取的占用 ​

保存为 main.go,执行 go run .。程序先构建 200 段文档,再走两遍:一遍 StreamExtractText,一遍 LoadBytes。runtime.GC() 之后的堆增量就是占用信号。

go
package main

import (
	"bytes"
	"fmt"
	"log"
	"runtime"

	"github.com/yunkeweb/go-word"
	"github.com/yunkeweb/go-word/style"
)

func heap() uint64 {
	runtime.GC()
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	return m.HeapAlloc
}

func main() {
	doc := word.New()
	sec := doc.AddSection()
	sec.AddTitle("Occupancy", 1)
	for i := 0; i < 200; i++ {
		sec.AddText("The quick brown fox jumps over the lazy dog.", style.Font{Size: 11})
	}
	raw, err := doc.Bytes()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("docx %d bytes\n", len(raw))

	before := heap()
	n := 0
	if err := word.StreamExtractText(bytes.NewReader(raw), func(p string) error {
		if p != "" {
			n++
		}
		return nil
	}); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("stream  paragraphs=%d  heapΔ=%d KiB\n", n, int64(heap()-before)/1024)

	before = heap()
	loaded, err := word.LoadBytes(raw)
	if err != nil {
		log.Fatal(err)
	}
	_ = loaded
	fmt.Printf("dom     heapΔ=%d KiB (tree retained)\n", int64(heap()-before)/1024)
}

在同一台 i7-10870H 上,把循环从 200 拉到几千段,流式那一趟的堆增量仍是数百 KiB 量级。DOM 那一趟随树增长。把打印出的 Δ 当成你这台机器上的趋势——HeapAlloc 不是实验室仪器。

如何复现 ​

sh
go test -bench=BenchmarkSaveDocx -benchmem -count=3
go test -bench=BenchmarkStreamWriter -benchmem -count=3

夹具在 bench_test.go:

  • benchDocument 构建 30 个带样式段落和一张 20×5 表。
  • BenchmarkTableRender 只构建 50×8 表。
  • BenchmarkTemplateProcess 每次迭代编译含两个占位符的模板。
  • BenchmarkStreamWriter 使用 NewStreamWriter。

完整示例 — 给一次 Save 计时 ​

go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/yunkeweb/go-word"
	"github.com/yunkeweb/go-word/style"
)

func main() {
	doc := word.New()
	sec := doc.AddSection()
	sec.AddTitle("Benchmark", 1)
	for i := 0; i < 30; i++ {
		sec.AddText("The quick brown fox jumps over the lazy dog.", style.Font{Size: 11})
	}
	tbl := sec.AddTable(style.Table{Width: 9000})
	for r := 0; r < 20; r++ {
		row := tbl.AddRow()
		for c := 0; c < 5; c++ {
			row.AddCell(1800).AddText("cell")
		}
	}
	start := time.Now()
	raw, err := doc.Bytes()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("docx %d bytes in %s\n", len(raw), time.Since(start))
}

不能把整棵 DOM 放进内存时,用 NewStreamWriter 写出,用 StreamExtractText 提取。

基于 GNU LGPL v3.0 发布。