Skip to content

log: optimize SetOutput(io.Discard) #47164

Closed
@Deleplace

Description

@Deleplace

Edit: as discussed in the comments, the proposal is not anymore about using a nil receiver. It is still about making it cheaper to call a disabled (muted) Logger.

What

Make this nil pointer variable

var nilLogger *log.Logger

a valid Logger, where all the calls to nilLogger.Print, etc. are valid (do not crash), and do not produce any actual logs.

This would serve as an adequate replacement of, and almost equivalent to:

var discardLogger = log.New(io.Discard, "", 0)

Why

Performance.

This is an important concern because logging is often expensive, and it is a reasonable expectation that it becomes free (or very cheap) when logging is turned off. Currently, discardLogger comes with a quite expensive overhead:

  1. all of the discardLogger.Print variadic arguments are evaluated, even if they are not really needed;
  2. a non-inlined function call to (*Logger).Printis executed
  3. fmt.Sprint is called, which incurs some memory allocation, and some work to use the variadic arguments in order to produce a string s;
  4. the mutex discardLogger.mu is Locked, then later Unlocked;
  5. s is copied to the internal buffer discardLogger.buf;
  6. a dynamic function dispatch is executed for discardLogger.out.Write, which resolves to io.discard.Write.

The proposed implementation consists of checking for nil receiver inside all of the exported methods and returning immediately, thus avoiding steps 3, 4, 5, 6.

In the proposed implementation, the steps 1, 2 still happen, as the arguments are still evaluated. In theory this could be mitigated in some side-effect-free cases, with advanced escape analysis and inlining. This mitigation is out of reach of the current inlining system, and out of scope of the current proposal.

Benefits

In a sample benchmark, nilLogger.Printf is 35x faster with an int arg, and 320x faster with a map arg, than discardLogger.Printf.

The speedup would be higher with more arguments, or more complex arguments.

As a side effect, the zero value for the pointer type *log.Logger becomes useful and safe, without initialization.

Backwards compatibility

  • No changes to all the non-nil *log.Logger instances.
  • log.New(io.Discard, "", 0) still works.
  • nilLogger.Print would not panic anymore. A program relying on the panic behavior would break. It is however not a promise (no guarantee) from the log package to have nilLogger.Print panic.
  • nilLogger.Print would not call fmt.Sprint anymore, resulting in some custom String methods to not be called. A program relying the side effects of custom String methods would break. It is however not a promise (no guarantee) from the log package to have valid Loggers always call fmt.Sprint.
  • A program may expect l.SetOutput(w) to always succeeds for any valid logger l. Such a program would break.
  • A program may expect l.SetPrefix(prefix) to force the subsequent value returned by l.Prefix(), for any valid logger l. Such a program would break.
  • A program may expect l.SetFlags(flag) to force the subsequent value returned by l.Flags(), for any valid logger l. Such a program would break.

Concurrency

It is my understanding that the proposed implementation does not introduce any new risks of data races.

For a nil receiver, all of the Print methods are thread-safe, and do not require a mutex.

Detailed behavior of each method

When the receiver l is nil:

  • (l *Logger).Fatal(v ...interface{})
  • (l *Logger).Fatalf(format string, v ...interface{})
  • (l *Logger).Fatalln(v ...interface{}):
    • don't log anything. Call os.Exit(1).
  • (l *Logger).Flags() int:
    • return 0.
  • (l *Logger).Output(calldepth int, s string) error:
    • do nothing. Return nil.
  • (l *Logger).Panic(v ...interface{})
  • (l *Logger).Panicf(format string, v ...interface{})
  • (l *Logger).Panicln(v ...interface{}):
    • don't log anything. Build the string s and call panic(s).
  • (l *Logger).Prefix() string:
    • return "".
  • (l *Logger).Print(v ...interface{})
  • (l *Logger).Printf(format string, v ...interface{})
  • (l *Logger).Println(v ...interface{}):
    • do nothing.
  • (l *Logger).SetFlags(flag int):
    • do nothing.
  • (l *Logger).SetOutput(w io.Writer):
    • panic. The caller would expect w to be subsequently used, which is not possible.
  • (l *Logger).SetPrefix(prefix string):
    • do nothing.
  • (l *Logger).Writer() io.Writer:
    • return nil.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions