From d9a6f3c92a7c96d5af9385271cf9f5122cc138d6 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 27 Jun 2016 16:58:52 -0700 Subject: [PATCH 1/4] benchmark --- columnize_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/columnize_test.go b/columnize_test.go index 7bec390..ea68936 100644 --- a/columnize_test.go +++ b/columnize_test.go @@ -1,6 +1,11 @@ package columnize -import "testing" +import ( + "fmt" + "testing" + + crand "crypto/rand" +) func TestListOfStringsInput(t *testing.T) { input := []string{ @@ -74,6 +79,42 @@ func TestColumnWidthCalculator(t *testing.T) { } } +func BenchmarkColumnWidthCalculator(b *testing.B) { + // Generate the input + input := []string{ + "UUID A | UUID B | Column C | Column D", + } + + format := "%s|%s|%s|%s" + short := "short" + + uuid := func() string { + buf := make([]byte, 16) + if _, err := crand.Read(buf); err != nil { + panic(fmt.Errorf("failed to read random bytes: %v", err)) + } + + return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", + buf[0:4], + buf[4:6], + buf[6:8], + buf[8:10], + buf[10:16]) + } + + for i := 0; i < 1000; i++ { + l := fmt.Sprintf(format, uuid(), uuid(), short, short) + input = append(input, l) + } + + config := DefaultConfig() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + Format(input, config) + } +} + func TestVariedInputSpacing(t *testing.T) { input := []string{ "Column A |Column B| Column C", From f3a615553b70e51647b3c8478ac9238a91973955 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 27 Jun 2016 17:44:41 -0700 Subject: [PATCH 2/4] Optimize memory allocations and speed of formatting --- columnize.go | 53 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/columnize.go b/columnize.go index d877859..36340c4 100644 --- a/columnize.go +++ b/columnize.go @@ -1,6 +1,7 @@ package columnize import ( + "bytes" "fmt" "strings" ) @@ -31,13 +32,14 @@ func DefaultConfig() *Config { // Returns a list of elements, each representing a single item which will // belong to a column of output. func getElementsFromLine(config *Config, line string) []interface{} { - elements := make([]interface{}, 0) - for _, field := range strings.Split(line, config.Delim) { + seperated := strings.Split(line, config.Delim) + elements := make([]interface{}, len(seperated)) + for i, field := range seperated { value := strings.TrimSpace(field) if value == "" && config.Empty != "" { value = config.Empty } - elements = append(elements, value) + elements[i] = value } return elements } @@ -45,7 +47,7 @@ func getElementsFromLine(config *Config, line string) []interface{} { // Examines a list of strings and determines how wide each column should be // considering all of the elements that need to be printed within it. func getWidthsFromLines(config *Config, lines []string) []int { - var widths []int + widths := make([]int, 0, 8) for _, line := range lines { elems := getElementsFromLine(config, line) @@ -65,18 +67,22 @@ func getWidthsFromLines(config *Config, lines []string) []int { // returns a sprintf-style format string which can be used to print output // aligned properly with other lines using the same widths set. func (c *Config) getStringFormat(widths []int, columns int) string { + // Create the buffer + // 8 bytes per character * 8 characters per column * ~num columns + buf := bytes.NewBuffer(make([]byte, 0, 8*len(widths)*8)) + // Start with the prefix, if any was given. - stringfmt := c.Prefix + buf.WriteString(c.Prefix) // Create the format string from the discovered widths for i := 0; i < columns && i < len(widths); i++ { if i == columns-1 { - stringfmt += "%s\n" + buf.WriteString("%s\n") } else { - stringfmt += fmt.Sprintf("%%-%ds%s", widths[i], c.Glue) + fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue) } } - return stringfmt + return buf.String() } // MergeConfig merges two config objects together and returns the resulting @@ -108,18 +114,41 @@ func MergeConfig(a, b *Config) *Config { // Format is the public-facing interface that takes either a plain string // or a list of strings and returns nicely aligned output. func Format(lines []string, config *Config) string { - var result string - conf := MergeConfig(DefaultConfig(), config) widths := getWidthsFromLines(conf, lines) + // Estimate the buffer size + glueSize := len(conf.Glue) + var size int + for _, w := range widths { + size += w + glueSize + } + size *= len(lines) + + // Create the buffer + buf := bytes.NewBuffer(make([]byte, 0, size)) + + // Create a cache for the string formats + fmtCache := make(map[int]string, 16) + // Create the formatted output using the format string for _, line := range lines { elems := getElementsFromLine(conf, line) - stringfmt := conf.getStringFormat(widths, len(elems)) - result += fmt.Sprintf(stringfmt, elems...) + + // Get the string format using cache + numElems := len(elems) + stringfmt, ok := fmtCache[numElems] + if !ok { + stringfmt = conf.getStringFormat(widths, len(elems)) + fmtCache[numElems] = stringfmt + } + + fmt.Fprintf(buf, stringfmt, elems...) } + // Get the string result + result := buf.String() + // Remove trailing newline without removing leading/trailing space if n := len(result); n > 0 && result[n-1] == '\n' { result = result[:n-1] From 20fe534f476b5bc99681bb23558a255fe31d615f Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 27 Jun 2016 21:16:34 -0700 Subject: [PATCH 3/4] Respond to comments --- columnize.go | 12 ++++++------ columnize_test.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/columnize.go b/columnize.go index 36340c4..307cfef 100644 --- a/columnize.go +++ b/columnize.go @@ -67,12 +67,12 @@ func getWidthsFromLines(config *Config, lines []string) []int { // returns a sprintf-style format string which can be used to print output // aligned properly with other lines using the same widths set. func (c *Config) getStringFormat(widths []int, columns int) string { - // Create the buffer - // 8 bytes per character * 8 characters per column * ~num columns - buf := bytes.NewBuffer(make([]byte, 0, 8*len(widths)*8)) + // Create the buffer with an estimate of the length + buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns)) - // Start with the prefix, if any was given. - buf.WriteString(c.Prefix) + // Start with the prefix, if any was given. The buffer will not return an + // error so it does not need to be handled + _, _ = buf.WriteString(c.Prefix) // Create the format string from the discovered widths for i := 0; i < columns && i < len(widths); i++ { @@ -139,7 +139,7 @@ func Format(lines []string, config *Config) string { numElems := len(elems) stringfmt, ok := fmtCache[numElems] if !ok { - stringfmt = conf.getStringFormat(widths, len(elems)) + stringfmt = conf.getStringFormat(widths, numElems) fmtCache[numElems] = stringfmt } diff --git a/columnize_test.go b/columnize_test.go index ea68936..493e61e 100644 --- a/columnize_test.go +++ b/columnize_test.go @@ -82,7 +82,7 @@ func TestColumnWidthCalculator(t *testing.T) { func BenchmarkColumnWidthCalculator(b *testing.B) { // Generate the input input := []string{ - "UUID A | UUID B | Column C | Column D", + "UUID A | UUID B | UUID C | Column D | Column E", } format := "%s|%s|%s|%s" @@ -103,7 +103,7 @@ func BenchmarkColumnWidthCalculator(b *testing.B) { } for i := 0; i < 1000; i++ { - l := fmt.Sprintf(format, uuid(), uuid(), short, short) + l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short) input = append(input, l) } From 2508af7109db72191d3a02e3dc5ace7c8ef91577 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 27 Jun 2016 22:23:58 -0700 Subject: [PATCH 4/4] remove blank variables --- columnize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/columnize.go b/columnize.go index 307cfef..9f0fe72 100644 --- a/columnize.go +++ b/columnize.go @@ -72,7 +72,7 @@ func (c *Config) getStringFormat(widths []int, columns int) string { // Start with the prefix, if any was given. The buffer will not return an // error so it does not need to be handled - _, _ = buf.WriteString(c.Prefix) + buf.WriteString(c.Prefix) // Create the format string from the discovered widths for i := 0; i < columns && i < len(widths); i++ {