[PATCH 12/12] docs: kdoc: Improve the output text accumulation

Jonathan Corbet posted 12 patches 3 months, 1 week ago
There is a newer version of this series
[PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Jonathan Corbet 3 months, 1 week ago
Building strings with repeated concatenation is somewhat inefficient in
Python; it is better to make a list and glom them all together at the end.
Add a small set of methods to the OutputFormat superclass to manage the
output string, and use them throughout.

Signed-off-by: Jonathan Corbet <corbet@lwn.net>
---
 scripts/lib/kdoc/kdoc_output.py | 185 +++++++++++++++++---------------
 1 file changed, 98 insertions(+), 87 deletions(-)

diff --git a/scripts/lib/kdoc/kdoc_output.py b/scripts/lib/kdoc/kdoc_output.py
index ea8914537ba0..d4aabdaa9c51 100644
--- a/scripts/lib/kdoc/kdoc_output.py
+++ b/scripts/lib/kdoc/kdoc_output.py
@@ -73,7 +73,19 @@ class OutputFormat:
         self.config = None
         self.no_doc_sections = False
 
-        self.data = ""
+    #
+    # Accumulation and management of the output text.
+    #
+    def reset_output(self):
+        self._output = []
+
+    def emit(self, text):
+        """Add a string to out output text"""
+        self._output.append(text)
+
+    def output(self):
+        """Obtain the accumulated output text"""
+        return ''.join(self._output)
 
     def set_config(self, config):
         """
@@ -180,32 +192,31 @@ class OutputFormat:
         Handles a single entry from kernel-doc parser
         """
 
-        self.data = ""
-
+        self.reset_output()
         dtype = args.type
 
         if dtype == "doc":
             self.out_doc(fname, name, args)
-            return self.data
+            return self.output()
 
         if not self.check_declaration(dtype, name, args):
-            return self.data
+            return self.output()
 
         if dtype == "function":
             self.out_function(fname, name, args)
-            return self.data
+            return self.output()
 
         if dtype == "enum":
             self.out_enum(fname, name, args)
-            return self.data
+            return self.output()
 
         if dtype == "typedef":
             self.out_typedef(fname, name, args)
-            return self.data
+            return self.output()
 
         if dtype in ["struct", "union"]:
             self.out_struct(fname, name, args)
-            return self.data
+            return self.output()
 
         # Warn if some type requires an output logic
         self.config.log.warning("doesn't now how to output '%s' block",
@@ -274,7 +285,7 @@ class RestFormat(OutputFormat):
 
         if self.enable_lineno and ln is not None:
             ln += 1
-            self.data += f".. LINENO {ln}\n"
+            self.emit(f".. LINENO {ln}\n")
 
     def output_highlight(self, args):
         """
@@ -326,7 +337,7 @@ class RestFormat(OutputFormat):
 
         # Print the output with the line prefix
         for line in output.strip("\n").split("\n"):
-            self.data += self.lineprefix + line + "\n"
+            self.emit(self.lineprefix + line + "\n")
 
     def out_section(self, args, out_docblock=False):
         """
@@ -343,15 +354,15 @@ class RestFormat(OutputFormat):
 
             if out_docblock:
                 if not self.out_mode == self.OUTPUT_INCLUDE:
-                    self.data += f".. _{section}:\n\n"
-                    self.data += f'{self.lineprefix}**{section}**\n\n'
+                    self.emit(f".. _{section}:\n\n")
+                    self.emit(f'{self.lineprefix}**{section}**\n\n')
             else:
-                self.data += f'{self.lineprefix}**{section}**\n\n'
+                self.emit(f'{self.lineprefix}**{section}**\n\n')
 
             self.print_lineno(args.section_start_lines.get(section, 0))
             self.output_highlight(text)
-            self.data += "\n"
-        self.data += "\n"
+            self.emit("\n")
+        self.emit("\n")
 
     def out_doc(self, fname, name, args):
         if not self.check_doc(name, args):
@@ -389,41 +400,41 @@ class RestFormat(OutputFormat):
 
         self.print_lineno(ln)
         if args.get('typedef') or not args.get('functiontype'):
-            self.data += f".. c:macro:: {name}\n\n"
+            self.emit(f".. c:macro:: {name}\n\n")
 
             if args.get('typedef'):
-                self.data += "   **Typedef**: "
+                self.emit("   **Typedef**: ")
                 self.lineprefix = ""
                 self.output_highlight(args.get('purpose', ""))
-                self.data += "\n\n**Syntax**\n\n"
-                self.data += f"  ``{signature}``\n\n"
+                self.emit("\n\n**Syntax**\n\n")
+                self.emit(f"  ``{signature}``\n\n")
             else:
-                self.data += f"``{signature}``\n\n"
+                self.emit(f"``{signature}``\n\n")
         else:
-            self.data += f".. c:function:: {signature}\n\n"
+            self.emit(f".. c:function:: {signature}\n\n")
 
         if not args.get('typedef'):
             self.print_lineno(ln)
             self.lineprefix = "   "
             self.output_highlight(args.get('purpose', ""))
-            self.data += "\n"
+            self.emit("\n")
 
         # Put descriptive text into a container (HTML <div>) to help set
         # function prototypes apart
         self.lineprefix = "  "
 
         if args.parameterlist:
-            self.data += ".. container:: kernelindent\n\n"
-            self.data += f"{self.lineprefix}**Parameters**\n\n"
+            self.emit(".. container:: kernelindent\n\n")
+            self.emit(f"{self.lineprefix}**Parameters**\n\n")
 
         for parameter in args.parameterlist:
             parameter_name = KernRe(r'\[.*').sub('', parameter)
             dtype = args.parametertypes.get(parameter, "")
 
             if dtype:
-                self.data += f"{self.lineprefix}``{dtype}``\n"
+                self.emit(f"{self.lineprefix}``{dtype}``\n")
             else:
-                self.data += f"{self.lineprefix}``{parameter}``\n"
+                self.emit(f"{self.lineprefix}``{parameter}``\n")
 
             self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
 
@@ -432,9 +443,9 @@ class RestFormat(OutputFormat):
                args.parameterdescs[parameter_name] != KernelDoc.undescribed:
 
                 self.output_highlight(args.parameterdescs[parameter_name])
-                self.data += "\n"
+                self.emit("\n")
             else:
-                self.data += f"{self.lineprefix}*undescribed*\n\n"
+                self.emit(f"{self.lineprefix}*undescribed*\n\n")
             self.lineprefix = "  "
 
         self.out_section(args)
@@ -445,26 +456,26 @@ class RestFormat(OutputFormat):
         oldprefix = self.lineprefix
         ln = args.declaration_start_line
 
-        self.data += f"\n\n.. c:enum:: {name}\n\n"
+        self.emit(f"\n\n.. c:enum:: {name}\n\n")
 
         self.print_lineno(ln)
         self.lineprefix = "  "
         self.output_highlight(args.get('purpose', ''))
-        self.data += "\n"
+        self.emit("\n")
 
-        self.data += ".. container:: kernelindent\n\n"
+        self.emit(".. container:: kernelindent\n\n")
         outer = self.lineprefix + "  "
         self.lineprefix = outer + "  "
-        self.data += f"{outer}**Constants**\n\n"
+        self.emit(f"{outer}**Constants**\n\n")
 
         for parameter in args.parameterlist:
-            self.data += f"{outer}``{parameter}``\n"
+            self.emit(f"{outer}``{parameter}``\n")
 
             if args.parameterdescs.get(parameter, '') != KernelDoc.undescribed:
                 self.output_highlight(args.parameterdescs[parameter])
             else:
-                self.data += f"{self.lineprefix}*undescribed*\n\n"
-            self.data += "\n"
+                self.emit(f"{self.lineprefix}*undescribed*\n\n")
+            self.emit("\n")
 
         self.lineprefix = oldprefix
         self.out_section(args)
@@ -474,14 +485,14 @@ class RestFormat(OutputFormat):
         oldprefix = self.lineprefix
         ln = args.declaration_start_line
 
-        self.data += f"\n\n.. c:type:: {name}\n\n"
+        self.emit(f"\n\n.. c:type:: {name}\n\n")
 
         self.print_lineno(ln)
         self.lineprefix = "   "
 
         self.output_highlight(args.get('purpose', ''))
 
-        self.data += "\n"
+        self.emit("\n")
 
         self.lineprefix = oldprefix
         self.out_section(args)
@@ -493,7 +504,7 @@ class RestFormat(OutputFormat):
         dtype = args.type
         ln = args.declaration_start_line
 
-        self.data += f"\n\n.. c:{dtype}:: {name}\n\n"
+        self.emit(f"\n\n.. c:{dtype}:: {name}\n\n")
 
         self.print_lineno(ln)
 
@@ -501,20 +512,20 @@ class RestFormat(OutputFormat):
         self.lineprefix += "  "
 
         self.output_highlight(purpose)
-        self.data += "\n"
+        self.emit("\n")
 
-        self.data += ".. container:: kernelindent\n\n"
-        self.data += f"{self.lineprefix}**Definition**::\n\n"
+        self.emit(".. container:: kernelindent\n\n")
+        self.emit(f"{self.lineprefix}**Definition**::\n\n")
 
         self.lineprefix = self.lineprefix + "  "
 
         declaration = declaration.replace("\t", self.lineprefix)
 
-        self.data += f"{self.lineprefix}{dtype} {name}" + ' {' + "\n"
-        self.data += f"{declaration}{self.lineprefix}" + "};\n\n"
+        self.emit(f"{self.lineprefix}{dtype} {name}" + ' {' + "\n")
+        self.emit(f"{declaration}{self.lineprefix}" + "};\n\n")
 
         self.lineprefix = "  "
-        self.data += f"{self.lineprefix}**Members**\n\n"
+        self.emit(f"{self.lineprefix}**Members**\n\n")
         for parameter in args.parameterlist:
             if not parameter or parameter.startswith("#"):
                 continue
@@ -526,15 +537,15 @@ class RestFormat(OutputFormat):
 
             self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
 
-            self.data += f"{self.lineprefix}``{parameter}``\n"
+            self.emit(f"{self.lineprefix}``{parameter}``\n")
 
             self.lineprefix = "    "
             self.output_highlight(args.parameterdescs[parameter_name])
             self.lineprefix = "  "
 
-            self.data += "\n"
+            self.emit("\n")
 
-        self.data += "\n"
+        self.emit("\n")
 
         self.lineprefix = oldprefix
         self.out_section(args)
@@ -610,33 +621,33 @@ class ManFormat(OutputFormat):
                 continue
 
             if line[0] == ".":
-                self.data += "\\&" + line + "\n"
+                self.emit("\\&" + line + "\n")
             else:
-                self.data += line + "\n"
+                self.emit(line + "\n")
 
     def out_doc(self, fname, name, args):
         if not self.check_doc(name, args):
             return
 
-        self.data += f'.TH "{self.modulename}" 9 "{self.modulename}" "{self.man_date}" "API Manual" LINUX' + "\n"
+        self.emit(f'.TH "{self.modulename}" 9 "{self.modulename}" "{self.man_date}" "API Manual" LINUX' + "\n")
 
         for section, text in args.sections.items():
-            self.data += f'.SH "{section}"' + "\n"
+            self.emit(f'.SH "{section}"' + "\n")
             self.output_highlight(text)
 
     def out_function(self, fname, name, args):
         """output function in man"""
 
-        self.data += f'.TH "{name}" 9 "{name}" "{self.man_date}" "Kernel Hacker\'s Manual" LINUX' + "\n"
+        self.emit(f'.TH "{name}" 9 "{name}" "{self.man_date}" "Kernel Hacker\'s Manual" LINUX' + "\n")
 
-        self.data += ".SH NAME\n"
-        self.data += f"{name} \\- {args['purpose']}\n"
+        self.emit(".SH NAME\n")
+        self.emit(f"{name} \\- {args['purpose']}\n")
 
-        self.data += ".SH SYNOPSIS\n"
+        self.emit(".SH SYNOPSIS\n")
         if args.get('functiontype', ''):
-            self.data += f'.B "{args["functiontype"]}" {name}' + "\n"
+            self.emit(f'.B "{args["functiontype"]}" {name}' + "\n")
         else:
-            self.data += f'.B "{name}' + "\n"
+            self.emit(f'.B "{name}' + "\n")
 
         count = 0
         parenth = "("
@@ -649,68 +660,68 @@ class ManFormat(OutputFormat):
             dtype = args.parametertypes.get(parameter, "")
             if function_pointer.match(dtype):
                 # Pointer-to-function
-                self.data += f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n"
+                self.emit(f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n")
             else:
                 dtype = KernRe(r'([^\*])$').sub(r'\1 ', dtype)
 
-                self.data += f'.BI "{parenth}{dtype}"  "{post}"' + "\n"
+                self.emit(f'.BI "{parenth}{dtype}"  "{post}"' + "\n")
             count += 1
             parenth = ""
 
         if args.parameterlist:
-            self.data += ".SH ARGUMENTS\n"
+            self.emit(".SH ARGUMENTS\n")
 
         for parameter in args.parameterlist:
             parameter_name = re.sub(r'\[.*', '', parameter)
 
-            self.data += f'.IP "{parameter}" 12' + "\n"
+            self.emit(f'.IP "{parameter}" 12' + "\n")
             self.output_highlight(args.parameterdescs.get(parameter_name, ""))
 
         for section, text in args.sections.items():
-            self.data += f'.SH "{section.upper()}"' + "\n"
+            self.emit(f'.SH "{section.upper()}"' + "\n")
             self.output_highlight(text)
 
     def out_enum(self, fname, name, args):
-        self.data += f'.TH "{self.modulename}" 9 "enum {name}" "{self.man_date}" "API Manual" LINUX' + "\n"
+        self.emit(f'.TH "{self.modulename}" 9 "enum {name}" "{self.man_date}" "API Manual" LINUX' + "\n")
 
-        self.data += ".SH NAME\n"
-        self.data += f"enum {name} \\- {args['purpose']}\n"
+        self.emit(".SH NAME\n")
+        self.emit(f"enum {name} \\- {args['purpose']}\n")
 
-        self.data += ".SH SYNOPSIS\n"
-        self.data += f"enum {name}" + " {\n"
+        self.emit(".SH SYNOPSIS\n")
+        self.emit(f"enum {name}" + " {\n")
 
         count = 0
         for parameter in args.parameterlist:
-            self.data += f'.br\n.BI "    {parameter}"' + "\n"
+            self.emit(f'.br\n.BI "    {parameter}"' + "\n")
             if count == len(args.parameterlist) - 1:
-                self.data += "\n};\n"
+                self.emit("\n};\n")
             else:
-                self.data += ", \n.br\n"
+                self.emit(", \n.br\n")
 
             count += 1
 
-        self.data += ".SH Constants\n"
+        self.emit(".SH Constants\n")
 
         for parameter in args.parameterlist:
             parameter_name = KernRe(r'\[.*').sub('', parameter)
-            self.data += f'.IP "{parameter}" 12' + "\n"
+            self.emit(f'.IP "{parameter}" 12' + "\n")
             self.output_highlight(args.parameterdescs.get(parameter_name, ""))
 
         for section, text in args.sections.items():
-            self.data += f'.SH "{section}"' + "\n"
+            self.emit(f'.SH "{section}"' + "\n")
             self.output_highlight(text)
 
     def out_typedef(self, fname, name, args):
         module = self.modulename
         purpose = args.get('purpose')
 
-        self.data += f'.TH "{module}" 9 "{name}" "{self.man_date}" "API Manual" LINUX' + "\n"
+        self.emit(f'.TH "{module}" 9 "{name}" "{self.man_date}" "API Manual" LINUX' + "\n")
 
-        self.data += ".SH NAME\n"
-        self.data += f"typedef {name} \\- {purpose}\n"
+        self.emit(".SH NAME\n")
+        self.emit(f"typedef {name} \\- {purpose}\n")
 
         for section, text in args.sections.items():
-            self.data += f'.SH "{section}"' + "\n"
+            self.emit(f'.SH "{section}"' + "\n")
             self.output_highlight(text)
 
     def out_struct(self, fname, name, args):
@@ -718,20 +729,20 @@ class ManFormat(OutputFormat):
         purpose = args.get('purpose')
         definition = args.get('definition')
 
-        self.data += f'.TH "{module}" 9 "{args.type} {name}" "{self.man_date}" "API Manual" LINUX' + "\n"
+        self.emit(f'.TH "{module}" 9 "{args.type} {name}" "{self.man_date}" "API Manual" LINUX' + "\n")
 
-        self.data += ".SH NAME\n"
-        self.data += f"{args.type} {name} \\- {purpose}\n"
+        self.emit(".SH NAME\n")
+        self.emit(f"{args.type} {name} \\- {purpose}\n")
 
         # Replace tabs with two spaces and handle newlines
         declaration = definition.replace("\t", "  ")
         declaration = KernRe(r"\n").sub('"\n.br\n.BI "', declaration)
 
-        self.data += ".SH SYNOPSIS\n"
-        self.data += f"{args.type} {name} " + "{" + "\n.br\n"
-        self.data += f'.BI "{declaration}\n' + "};\n.br\n\n"
+        self.emit(".SH SYNOPSIS\n")
+        self.emit(f"{args.type} {name} " + "{" + "\n.br\n")
+        self.emit(f'.BI "{declaration}\n' + "};\n.br\n\n")
 
-        self.data += ".SH Members\n"
+        self.emit(".SH Members\n")
         for parameter in args.parameterlist:
             if parameter.startswith("#"):
                 continue
@@ -741,9 +752,9 @@ class ManFormat(OutputFormat):
             if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
                 continue
 
-            self.data += f'.IP "{parameter}" 12' + "\n"
+            self.emit(f'.IP "{parameter}" 12' + "\n")
             self.output_highlight(args.parameterdescs.get(parameter_name))
 
         for section, text in args.sections.items():
-            self.data += f'.SH "{section}"' + "\n"
+            self.emit(f'.SH "{section}"' + "\n")
             self.output_highlight(text)
-- 
2.49.0
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Wed,  2 Jul 2025 16:35:24 -0600
Jonathan Corbet <corbet@lwn.net> escreveu:

> Building strings with repeated concatenation is somewhat inefficient in
> Python; it is better to make a list and glom them all together at the end.
> Add a small set of methods to the OutputFormat superclass to manage the
> output string, and use them throughout.
> 
> Signed-off-by: Jonathan Corbet <corbet@lwn.net>

The patch looks good to me. Just a minor nit below.

> ---
>  scripts/lib/kdoc/kdoc_output.py | 185 +++++++++++++++++---------------
>  1 file changed, 98 insertions(+), 87 deletions(-)
> 
> diff --git a/scripts/lib/kdoc/kdoc_output.py b/scripts/lib/kdoc/kdoc_output.py
> index ea8914537ba0..d4aabdaa9c51 100644
> --- a/scripts/lib/kdoc/kdoc_output.py
> +++ b/scripts/lib/kdoc/kdoc_output.py
> @@ -73,7 +73,19 @@ class OutputFormat:
>          self.config = None
>          self.no_doc_sections = False
>  
> -        self.data = ""
> +    #
> +    # Accumulation and management of the output text.
> +    #
> +    def reset_output(self):
> +        self._output = []
> +
> +    def emit(self, text):
> +        """Add a string to out output text"""
> +        self._output.append(text)
> +
> +    def output(self):
> +        """Obtain the accumulated output text"""
> +        return ''.join(self._output)

I would prefer to use a more Pythonic name for this function:

	def __str__(self)

This way, all it takes to get the final string is to use str():

	out_str = str(out)

With that:

Reviewed-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>

>  
>      def set_config(self, config):
>          """
> @@ -180,32 +192,31 @@ class OutputFormat:
>          Handles a single entry from kernel-doc parser
>          """
>  
> -        self.data = ""
> -
> +        self.reset_output()
>          dtype = args.type
>  
>          if dtype == "doc":
>              self.out_doc(fname, name, args)
> -            return self.data
> +            return self.output()
>  
>          if not self.check_declaration(dtype, name, args):
> -            return self.data
> +            return self.output()
>  
>          if dtype == "function":
>              self.out_function(fname, name, args)
> -            return self.data
> +            return self.output()
>  
>          if dtype == "enum":
>              self.out_enum(fname, name, args)
> -            return self.data
> +            return self.output()
>  
>          if dtype == "typedef":
>              self.out_typedef(fname, name, args)
> -            return self.data
> +            return self.output()
>  
>          if dtype in ["struct", "union"]:
>              self.out_struct(fname, name, args)
> -            return self.data
> +            return self.output()
>  
>          # Warn if some type requires an output logic
>          self.config.log.warning("doesn't now how to output '%s' block",
> @@ -274,7 +285,7 @@ class RestFormat(OutputFormat):
>  
>          if self.enable_lineno and ln is not None:
>              ln += 1
> -            self.data += f".. LINENO {ln}\n"
> +            self.emit(f".. LINENO {ln}\n")
>  
>      def output_highlight(self, args):
>          """
> @@ -326,7 +337,7 @@ class RestFormat(OutputFormat):
>  
>          # Print the output with the line prefix
>          for line in output.strip("\n").split("\n"):
> -            self.data += self.lineprefix + line + "\n"
> +            self.emit(self.lineprefix + line + "\n")
>  
>      def out_section(self, args, out_docblock=False):
>          """
> @@ -343,15 +354,15 @@ class RestFormat(OutputFormat):
>  
>              if out_docblock:
>                  if not self.out_mode == self.OUTPUT_INCLUDE:
> -                    self.data += f".. _{section}:\n\n"
> -                    self.data += f'{self.lineprefix}**{section}**\n\n'
> +                    self.emit(f".. _{section}:\n\n")
> +                    self.emit(f'{self.lineprefix}**{section}**\n\n')
>              else:
> -                self.data += f'{self.lineprefix}**{section}**\n\n'
> +                self.emit(f'{self.lineprefix}**{section}**\n\n')
>  
>              self.print_lineno(args.section_start_lines.get(section, 0))
>              self.output_highlight(text)
> -            self.data += "\n"
> -        self.data += "\n"
> +            self.emit("\n")
> +        self.emit("\n")
>  
>      def out_doc(self, fname, name, args):
>          if not self.check_doc(name, args):
> @@ -389,41 +400,41 @@ class RestFormat(OutputFormat):
>  
>          self.print_lineno(ln)
>          if args.get('typedef') or not args.get('functiontype'):
> -            self.data += f".. c:macro:: {name}\n\n"
> +            self.emit(f".. c:macro:: {name}\n\n")
>  
>              if args.get('typedef'):
> -                self.data += "   **Typedef**: "
> +                self.emit("   **Typedef**: ")
>                  self.lineprefix = ""
>                  self.output_highlight(args.get('purpose', ""))
> -                self.data += "\n\n**Syntax**\n\n"
> -                self.data += f"  ``{signature}``\n\n"
> +                self.emit("\n\n**Syntax**\n\n")
> +                self.emit(f"  ``{signature}``\n\n")
>              else:
> -                self.data += f"``{signature}``\n\n"
> +                self.emit(f"``{signature}``\n\n")
>          else:
> -            self.data += f".. c:function:: {signature}\n\n"
> +            self.emit(f".. c:function:: {signature}\n\n")
>  
>          if not args.get('typedef'):
>              self.print_lineno(ln)
>              self.lineprefix = "   "
>              self.output_highlight(args.get('purpose', ""))
> -            self.data += "\n"
> +            self.emit("\n")
>  
>          # Put descriptive text into a container (HTML <div>) to help set
>          # function prototypes apart
>          self.lineprefix = "  "
>  
>          if args.parameterlist:
> -            self.data += ".. container:: kernelindent\n\n"
> -            self.data += f"{self.lineprefix}**Parameters**\n\n"
> +            self.emit(".. container:: kernelindent\n\n")
> +            self.emit(f"{self.lineprefix}**Parameters**\n\n")
>  
>          for parameter in args.parameterlist:
>              parameter_name = KernRe(r'\[.*').sub('', parameter)
>              dtype = args.parametertypes.get(parameter, "")
>  
>              if dtype:
> -                self.data += f"{self.lineprefix}``{dtype}``\n"
> +                self.emit(f"{self.lineprefix}``{dtype}``\n")
>              else:
> -                self.data += f"{self.lineprefix}``{parameter}``\n"
> +                self.emit(f"{self.lineprefix}``{parameter}``\n")
>  
>              self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
>  
> @@ -432,9 +443,9 @@ class RestFormat(OutputFormat):
>                 args.parameterdescs[parameter_name] != KernelDoc.undescribed:
>  
>                  self.output_highlight(args.parameterdescs[parameter_name])
> -                self.data += "\n"
> +                self.emit("\n")
>              else:
> -                self.data += f"{self.lineprefix}*undescribed*\n\n"
> +                self.emit(f"{self.lineprefix}*undescribed*\n\n")
>              self.lineprefix = "  "
>  
>          self.out_section(args)
> @@ -445,26 +456,26 @@ class RestFormat(OutputFormat):
>          oldprefix = self.lineprefix
>          ln = args.declaration_start_line
>  
> -        self.data += f"\n\n.. c:enum:: {name}\n\n"
> +        self.emit(f"\n\n.. c:enum:: {name}\n\n")
>  
>          self.print_lineno(ln)
>          self.lineprefix = "  "
>          self.output_highlight(args.get('purpose', ''))
> -        self.data += "\n"
> +        self.emit("\n")
>  
> -        self.data += ".. container:: kernelindent\n\n"
> +        self.emit(".. container:: kernelindent\n\n")
>          outer = self.lineprefix + "  "
>          self.lineprefix = outer + "  "
> -        self.data += f"{outer}**Constants**\n\n"
> +        self.emit(f"{outer}**Constants**\n\n")
>  
>          for parameter in args.parameterlist:
> -            self.data += f"{outer}``{parameter}``\n"
> +            self.emit(f"{outer}``{parameter}``\n")
>  
>              if args.parameterdescs.get(parameter, '') != KernelDoc.undescribed:
>                  self.output_highlight(args.parameterdescs[parameter])
>              else:
> -                self.data += f"{self.lineprefix}*undescribed*\n\n"
> -            self.data += "\n"
> +                self.emit(f"{self.lineprefix}*undescribed*\n\n")
> +            self.emit("\n")
>  
>          self.lineprefix = oldprefix
>          self.out_section(args)
> @@ -474,14 +485,14 @@ class RestFormat(OutputFormat):
>          oldprefix = self.lineprefix
>          ln = args.declaration_start_line
>  
> -        self.data += f"\n\n.. c:type:: {name}\n\n"
> +        self.emit(f"\n\n.. c:type:: {name}\n\n")
>  
>          self.print_lineno(ln)
>          self.lineprefix = "   "
>  
>          self.output_highlight(args.get('purpose', ''))
>  
> -        self.data += "\n"
> +        self.emit("\n")
>  
>          self.lineprefix = oldprefix
>          self.out_section(args)
> @@ -493,7 +504,7 @@ class RestFormat(OutputFormat):
>          dtype = args.type
>          ln = args.declaration_start_line
>  
> -        self.data += f"\n\n.. c:{dtype}:: {name}\n\n"
> +        self.emit(f"\n\n.. c:{dtype}:: {name}\n\n")
>  
>          self.print_lineno(ln)
>  
> @@ -501,20 +512,20 @@ class RestFormat(OutputFormat):
>          self.lineprefix += "  "
>  
>          self.output_highlight(purpose)
> -        self.data += "\n"
> +        self.emit("\n")
>  
> -        self.data += ".. container:: kernelindent\n\n"
> -        self.data += f"{self.lineprefix}**Definition**::\n\n"
> +        self.emit(".. container:: kernelindent\n\n")
> +        self.emit(f"{self.lineprefix}**Definition**::\n\n")
>  
>          self.lineprefix = self.lineprefix + "  "
>  
>          declaration = declaration.replace("\t", self.lineprefix)
>  
> -        self.data += f"{self.lineprefix}{dtype} {name}" + ' {' + "\n"
> -        self.data += f"{declaration}{self.lineprefix}" + "};\n\n"
> +        self.emit(f"{self.lineprefix}{dtype} {name}" + ' {' + "\n")
> +        self.emit(f"{declaration}{self.lineprefix}" + "};\n\n")
>  
>          self.lineprefix = "  "
> -        self.data += f"{self.lineprefix}**Members**\n\n"
> +        self.emit(f"{self.lineprefix}**Members**\n\n")
>          for parameter in args.parameterlist:
>              if not parameter or parameter.startswith("#"):
>                  continue
> @@ -526,15 +537,15 @@ class RestFormat(OutputFormat):
>  
>              self.print_lineno(args.parameterdesc_start_lines.get(parameter_name, 0))
>  
> -            self.data += f"{self.lineprefix}``{parameter}``\n"
> +            self.emit(f"{self.lineprefix}``{parameter}``\n")
>  
>              self.lineprefix = "    "
>              self.output_highlight(args.parameterdescs[parameter_name])
>              self.lineprefix = "  "
>  
> -            self.data += "\n"
> +            self.emit("\n")
>  
> -        self.data += "\n"
> +        self.emit("\n")
>  
>          self.lineprefix = oldprefix
>          self.out_section(args)
> @@ -610,33 +621,33 @@ class ManFormat(OutputFormat):
>                  continue
>  
>              if line[0] == ".":
> -                self.data += "\\&" + line + "\n"
> +                self.emit("\\&" + line + "\n")
>              else:
> -                self.data += line + "\n"
> +                self.emit(line + "\n")
>  
>      def out_doc(self, fname, name, args):
>          if not self.check_doc(name, args):
>              return
>  
> -        self.data += f'.TH "{self.modulename}" 9 "{self.modulename}" "{self.man_date}" "API Manual" LINUX' + "\n"
> +        self.emit(f'.TH "{self.modulename}" 9 "{self.modulename}" "{self.man_date}" "API Manual" LINUX' + "\n")
>  
>          for section, text in args.sections.items():
> -            self.data += f'.SH "{section}"' + "\n"
> +            self.emit(f'.SH "{section}"' + "\n")
>              self.output_highlight(text)
>  
>      def out_function(self, fname, name, args):
>          """output function in man"""
>  
> -        self.data += f'.TH "{name}" 9 "{name}" "{self.man_date}" "Kernel Hacker\'s Manual" LINUX' + "\n"
> +        self.emit(f'.TH "{name}" 9 "{name}" "{self.man_date}" "Kernel Hacker\'s Manual" LINUX' + "\n")
>  
> -        self.data += ".SH NAME\n"
> -        self.data += f"{name} \\- {args['purpose']}\n"
> +        self.emit(".SH NAME\n")
> +        self.emit(f"{name} \\- {args['purpose']}\n")
>  
> -        self.data += ".SH SYNOPSIS\n"
> +        self.emit(".SH SYNOPSIS\n")
>          if args.get('functiontype', ''):
> -            self.data += f'.B "{args["functiontype"]}" {name}' + "\n"
> +            self.emit(f'.B "{args["functiontype"]}" {name}' + "\n")
>          else:
> -            self.data += f'.B "{name}' + "\n"
> +            self.emit(f'.B "{name}' + "\n")
>  
>          count = 0
>          parenth = "("
> @@ -649,68 +660,68 @@ class ManFormat(OutputFormat):
>              dtype = args.parametertypes.get(parameter, "")
>              if function_pointer.match(dtype):
>                  # Pointer-to-function
> -                self.data += f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n"
> +                self.emit(f'".BI "{parenth}{function_pointer.group(1)}" " ") ({function_pointer.group(2)}){post}"' + "\n")
>              else:
>                  dtype = KernRe(r'([^\*])$').sub(r'\1 ', dtype)
>  
> -                self.data += f'.BI "{parenth}{dtype}"  "{post}"' + "\n"
> +                self.emit(f'.BI "{parenth}{dtype}"  "{post}"' + "\n")
>              count += 1
>              parenth = ""
>  
>          if args.parameterlist:
> -            self.data += ".SH ARGUMENTS\n"
> +            self.emit(".SH ARGUMENTS\n")
>  
>          for parameter in args.parameterlist:
>              parameter_name = re.sub(r'\[.*', '', parameter)
>  
> -            self.data += f'.IP "{parameter}" 12' + "\n"
> +            self.emit(f'.IP "{parameter}" 12' + "\n")
>              self.output_highlight(args.parameterdescs.get(parameter_name, ""))
>  
>          for section, text in args.sections.items():
> -            self.data += f'.SH "{section.upper()}"' + "\n"
> +            self.emit(f'.SH "{section.upper()}"' + "\n")
>              self.output_highlight(text)
>  
>      def out_enum(self, fname, name, args):
> -        self.data += f'.TH "{self.modulename}" 9 "enum {name}" "{self.man_date}" "API Manual" LINUX' + "\n"
> +        self.emit(f'.TH "{self.modulename}" 9 "enum {name}" "{self.man_date}" "API Manual" LINUX' + "\n")
>  
> -        self.data += ".SH NAME\n"
> -        self.data += f"enum {name} \\- {args['purpose']}\n"
> +        self.emit(".SH NAME\n")
> +        self.emit(f"enum {name} \\- {args['purpose']}\n")
>  
> -        self.data += ".SH SYNOPSIS\n"
> -        self.data += f"enum {name}" + " {\n"
> +        self.emit(".SH SYNOPSIS\n")
> +        self.emit(f"enum {name}" + " {\n")
>  
>          count = 0
>          for parameter in args.parameterlist:
> -            self.data += f'.br\n.BI "    {parameter}"' + "\n"
> +            self.emit(f'.br\n.BI "    {parameter}"' + "\n")
>              if count == len(args.parameterlist) - 1:
> -                self.data += "\n};\n"
> +                self.emit("\n};\n")
>              else:
> -                self.data += ", \n.br\n"
> +                self.emit(", \n.br\n")
>  
>              count += 1
>  
> -        self.data += ".SH Constants\n"
> +        self.emit(".SH Constants\n")
>  
>          for parameter in args.parameterlist:
>              parameter_name = KernRe(r'\[.*').sub('', parameter)
> -            self.data += f'.IP "{parameter}" 12' + "\n"
> +            self.emit(f'.IP "{parameter}" 12' + "\n")
>              self.output_highlight(args.parameterdescs.get(parameter_name, ""))
>  
>          for section, text in args.sections.items():
> -            self.data += f'.SH "{section}"' + "\n"
> +            self.emit(f'.SH "{section}"' + "\n")
>              self.output_highlight(text)
>  
>      def out_typedef(self, fname, name, args):
>          module = self.modulename
>          purpose = args.get('purpose')
>  
> -        self.data += f'.TH "{module}" 9 "{name}" "{self.man_date}" "API Manual" LINUX' + "\n"
> +        self.emit(f'.TH "{module}" 9 "{name}" "{self.man_date}" "API Manual" LINUX' + "\n")
>  
> -        self.data += ".SH NAME\n"
> -        self.data += f"typedef {name} \\- {purpose}\n"
> +        self.emit(".SH NAME\n")
> +        self.emit(f"typedef {name} \\- {purpose}\n")
>  
>          for section, text in args.sections.items():
> -            self.data += f'.SH "{section}"' + "\n"
> +            self.emit(f'.SH "{section}"' + "\n")
>              self.output_highlight(text)
>  
>      def out_struct(self, fname, name, args):
> @@ -718,20 +729,20 @@ class ManFormat(OutputFormat):
>          purpose = args.get('purpose')
>          definition = args.get('definition')
>  
> -        self.data += f'.TH "{module}" 9 "{args.type} {name}" "{self.man_date}" "API Manual" LINUX' + "\n"
> +        self.emit(f'.TH "{module}" 9 "{args.type} {name}" "{self.man_date}" "API Manual" LINUX' + "\n")
>  
> -        self.data += ".SH NAME\n"
> -        self.data += f"{args.type} {name} \\- {purpose}\n"
> +        self.emit(".SH NAME\n")
> +        self.emit(f"{args.type} {name} \\- {purpose}\n")
>  
>          # Replace tabs with two spaces and handle newlines
>          declaration = definition.replace("\t", "  ")
>          declaration = KernRe(r"\n").sub('"\n.br\n.BI "', declaration)
>  
> -        self.data += ".SH SYNOPSIS\n"
> -        self.data += f"{args.type} {name} " + "{" + "\n.br\n"
> -        self.data += f'.BI "{declaration}\n' + "};\n.br\n\n"
> +        self.emit(".SH SYNOPSIS\n")
> +        self.emit(f"{args.type} {name} " + "{" + "\n.br\n")
> +        self.emit(f'.BI "{declaration}\n' + "};\n.br\n\n")
>  
> -        self.data += ".SH Members\n"
> +        self.emit(".SH Members\n")
>          for parameter in args.parameterlist:
>              if parameter.startswith("#"):
>                  continue
> @@ -741,9 +752,9 @@ class ManFormat(OutputFormat):
>              if args.parameterdescs.get(parameter_name) == KernelDoc.undescribed:
>                  continue
>  
> -            self.data += f'.IP "{parameter}" 12' + "\n"
> +            self.emit(f'.IP "{parameter}" 12' + "\n")
>              self.output_highlight(args.parameterdescs.get(parameter_name))
>  
>          for section, text in args.sections.items():
> -            self.data += f'.SH "{section}"' + "\n"
> +            self.emit(f'.SH "{section}"' + "\n")
>              self.output_highlight(text)



Thanks,
Mauro
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Thu, 10 Jul 2025 08:41:19 +0200
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:

> Em Wed,  2 Jul 2025 16:35:24 -0600
> Jonathan Corbet <corbet@lwn.net> escreveu:
> 
> > Building strings with repeated concatenation is somewhat inefficient in
> > Python; it is better to make a list and glom them all together at the end.
> > Add a small set of methods to the OutputFormat superclass to manage the
> > output string, and use them throughout.
> > 
> > Signed-off-by: Jonathan Corbet <corbet@lwn.net>  
> 
> The patch looks good to me. Just a minor nit below.
> 
> > ---
> >  scripts/lib/kdoc/kdoc_output.py | 185 +++++++++++++++++---------------
> >  1 file changed, 98 insertions(+), 87 deletions(-)
> > 
> > diff --git a/scripts/lib/kdoc/kdoc_output.py b/scripts/lib/kdoc/kdoc_output.py
> > index ea8914537ba0..d4aabdaa9c51 100644
> > --- a/scripts/lib/kdoc/kdoc_output.py
> > +++ b/scripts/lib/kdoc/kdoc_output.py
> > @@ -73,7 +73,19 @@ class OutputFormat:
> >          self.config = None
> >          self.no_doc_sections = False
> >  
> > -        self.data = ""
> > +    #
> > +    # Accumulation and management of the output text.
> > +    #
> > +    def reset_output(self):
> > +        self._output = []
> > +
> > +    def emit(self, text):
> > +        """Add a string to out output text"""
> > +        self._output.append(text)
> > +
> > +    def output(self):
> > +        """Obtain the accumulated output text"""
> > +        return ''.join(self._output)  
> 
> I would prefer to use a more Pythonic name for this function:
> 
> 	def __str__(self)
> 
> This way, all it takes to get the final string is to use str():
> 
> 	out_str = str(out)
> 
> With that:
> 
> Reviewed-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>


Hmm... actually, I would code it on a different way, using something like:

class OutputString:
    def __init__(self):
	"""Initialize internal list"""
        self._output = []
    
    # Probably not needed - The code can simply do, instead:
    # a = OutputString() to create a new string.
    def reset(self):
        """Reset the output text"""
        self._output = []
    
    def __add__(self, text):
	"""Add a string to out output text"""
        if not isinstance(text, str):
            raise TypeError("Can only append strings")
        self._output.append(text)
        return self

    def __str__(self):
        return ''.join(self._output)
    
    # and, if needed, add a getter/setter:

    @property
    def data(self):
        """Getter for the current output"""
        return ''.join(self._output)

    @data.setter
    def data(self, new_value):
        if isinstance(new_value, str):
	    self._output = [new_value]
	elif isinstance(new_value, list):
            self._output = new_value
        else:
            raise TypeError("Value should be either list or string")

That would allow things like:

	out = OutputString()
	out = out + "Foo" + " " + "Bar"
	print(out)

	out = OutputString()
	out += "Foo"
	out += " "
	out += "Bar"
	return(str(out))

and won't require much changes at the output logic, and IMO will
provide a cleaner code.

Thanks,
Mauro
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Thu, 10 Jul 2025 09:13:52 +0200
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:

> Em Thu, 10 Jul 2025 08:41:19 +0200
> Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:
> 
> > Em Wed,  2 Jul 2025 16:35:24 -0600
> > Jonathan Corbet <corbet@lwn.net> escreveu:
> >   
> > > Building strings with repeated concatenation is somewhat inefficient in
> > > Python; it is better to make a list and glom them all together at the end.
> > > Add a small set of methods to the OutputFormat superclass to manage the
> > > output string, and use them throughout.
> > > 
> > > Signed-off-by: Jonathan Corbet <corbet@lwn.net>    
> > 
> > The patch looks good to me. Just a minor nit below.
> >   
> > > ---
> > >  scripts/lib/kdoc/kdoc_output.py | 185 +++++++++++++++++---------------
> > >  1 file changed, 98 insertions(+), 87 deletions(-)
> > > 
> > > diff --git a/scripts/lib/kdoc/kdoc_output.py b/scripts/lib/kdoc/kdoc_output.py
> > > index ea8914537ba0..d4aabdaa9c51 100644
> > > --- a/scripts/lib/kdoc/kdoc_output.py
> > > +++ b/scripts/lib/kdoc/kdoc_output.py
> > > @@ -73,7 +73,19 @@ class OutputFormat:
> > >          self.config = None
> > >          self.no_doc_sections = False
> > >  
> > > -        self.data = ""
> > > +    #
> > > +    # Accumulation and management of the output text.
> > > +    #
> > > +    def reset_output(self):
> > > +        self._output = []
> > > +
> > > +    def emit(self, text):
> > > +        """Add a string to out output text"""
> > > +        self._output.append(text)
> > > +
> > > +    def output(self):
> > > +        """Obtain the accumulated output text"""
> > > +        return ''.join(self._output)    
> > 
> > I would prefer to use a more Pythonic name for this function:
> > 
> > 	def __str__(self)
> > 
> > This way, all it takes to get the final string is to use str():
> > 
> > 	out_str = str(out)
> > 
> > With that:
> > 
> > Reviewed-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>  
> 
> 
> Hmm... actually, I would code it on a different way, using something like:
> 
> class OutputString:
>     def __init__(self):
> 	"""Initialize internal list"""
>         self._output = []
>     
>     # Probably not needed - The code can simply do, instead:
>     # a = OutputString() to create a new string.
>     def reset(self):
>         """Reset the output text"""
>         self._output = []
>     
>     def __add__(self, text):
> 	"""Add a string to out output text"""
>         if not isinstance(text, str):
>             raise TypeError("Can only append strings")
>         self._output.append(text)
>         return self
> 
>     def __str__(self):
>         return ''.join(self._output)
>     
>     # and, if needed, add a getter/setter:
> 
>     @property
>     def data(self):
>         """Getter for the current output"""
>         return ''.join(self._output)
> 
>     @data.setter
>     def data(self, new_value):
>         if isinstance(new_value, str):
> 	    self._output = [new_value]
> 	elif isinstance(new_value, list):
>             self._output = new_value
>         else:
>             raise TypeError("Value should be either list or string")
> 
> That would allow things like:
> 
> 	out = OutputString()
> 	out = out + "Foo" + " " + "Bar"
> 	print(out)
> 
> 	out = OutputString()
> 	out += "Foo"
> 	out += " "
> 	out += "Bar"
> 	return(str(out))
> 
> and won't require much changes at the output logic, and IMO will
> provide a cleaner code.
> 
> Thanks,
> Mauro

Heh, on those times where LLM can quickly code trivial things for us,
I actually decided to test 3 different variants:

- using string +=
- using list append
- using __add__
- using __iadd__

Except if the LLM-generated did something wrong (I double checked, and
was unable to find any issues), the results on Python 3.13.5 are:

$ /tmp/bench.py
Benchmarking 1,000 ops × 1000 strings = 1,000,000 appends

Run    str+=        ExplicitList __add__      __iadd__    
------------------------------------------------------------
1      25.26        29.44        53.42        50.71       
2      29.34        29.35        53.45        50.61       
3      29.44        29.56        53.41        50.67       
4      29.28        29.23        53.26        50.64       
5      29.28        29.20        45.90        40.47       
6      23.53        23.62        42.74        40.61       
7      23.43        23.76        42.97        40.78       
8      23.51        23.59        42.67        40.61       
9      23.43        23.52        42.77        40.72       
10     23.53        23.54        42.78        40.67       
11     23.83        23.63        42.98        40.87       
12     23.49        23.45        42.67        40.53       
13     23.43        23.69        42.75        40.66       
14     23.47        23.49        42.70        40.56       
15     23.44        23.63        42.72        40.52       
16     23.51        23.56        42.65        40.66       
17     23.48        23.60        42.86        40.81       
18     23.67        23.53        42.73        40.59       
19     23.75        23.62        42.78        40.58       
20     23.68        23.55        42.77        40.54       
21     23.65        23.67        42.76        40.59       
22     23.73        23.49        42.78        40.61       
23     23.61        23.59        42.78        40.58       
24     23.66        23.51        42.73        40.55       
------------------------------------------------------------
Avg    24.60        24.78        44.67        42.30       

Summary:
ExplicitList : 100.74% slower than str+=
__add__      : 181.56% slower than str+=
__iadd__     : 171.93% slower than str+=

(running it a couple of times sometimes it sometimes show list
 addition a little bit better, bu all at the +/- 1% range)

In practice, it means that my suggestion of using __add__ (or even
using the __iadd__ variant) was not good, but it also showed
that Python 3.13 implementation is actually very efficient
with str += operations.

With that, I would just drop this patch, as the performance is
almost identical, and using "emit()" instead of "+=" IMO makes
the code less clear.

-

Btw, with Python 3.9, "".join(list) is a lot worse than str += :

$ python3.9 /tmp/bench.py
Benchmarking 1,000 ops × 1000 strings = 1,000,000 appends

Run    str+=        ExplicitList __add__      __iadd__    
------------------------------------------------------------
1      28.27        87.24        96.03        88.81       
2      32.76        87.35        87.40        88.92       
3      32.69        85.98        73.01        70.87       
4      26.28        69.80        70.62        71.90       
5      27.21        70.54        71.04        72.00       
6      27.77        70.06        70.22        70.92       
7      27.03        69.75        70.30        70.89       
8      33.31        72.63        70.57        70.59       
9      26.33        70.15        70.27        70.97       
10     26.29        69.84        71.60        70.94       
11     26.59        69.60        70.16        71.26       
12     26.38        69.57        71.64        70.95       
13     26.41        69.89        70.11        70.85       
14     26.38        69.86        70.36        70.93       
15     26.43        69.57        70.18        70.90       
16     26.38        70.04        70.26        71.19       
17     26.40        70.02        80.50        71.01       
18     26.41        71.74        80.39        71.90       
19     28.06        69.60        71.95        70.88       
20     28.28        69.90        71.12        71.07       
21     26.34        69.74        72.42        71.02       
22     26.33        69.86        70.25        70.97       
23     26.40        69.78        71.64        71.10       
24     26.44        69.73        70.23        70.83       
------------------------------------------------------------
Avg    27.55        72.18        73.43        72.57       

Summary:
ExplicitList : 262.00% slower than str+=
__add__      : 266.54% slower than str+=
__iadd__     : 263.42% slower than str+=


Thanks,
Mauro

---

#!/usr/bin/env python3

import timeit

class ExplicitList:
    def __init__(self):
        self._output = []

    def emit(self, text):
        self._output.append(text)

    def output(self):
        return ''.join(self._output)

class OutputStringAdd:
    def __init__(self):
        self._output = []

    def __add__(self, text):
        self._output.append(text)
        return self

    def __str__(self):
        return ''.join(self._output)

class OutputStringIAdd:
    def __init__(self):
        self._output = []

    def __iadd__(self, text):
        self._output.append(text)
        return self

    def __str__(self):
        return ''.join(self._output)

def calculate_comparison(base_time, compare_time):
    """Returns tuple of (is_faster, percentage)"""
    if compare_time < base_time:
        return (True, (1 - compare_time/base_time)*100)
    else:
        return (False, (compare_time/base_time)*100)

def benchmark():
    N = 1000       # Operations
    STRINGS_PER_OP = 1000
    REPEATS = 24

    # Generate test data (1000 unique 10-character strings)
    test_strings = [f"string_{i:03d}" for i in range(STRINGS_PER_OP)]

    print(f"Benchmarking {N:,} ops × {STRINGS_PER_OP} strings = {N*STRINGS_PER_OP:,} appends\n")
    headers = ['Run', 'str+=', 'ExplicitList', '__add__', '__iadd__']
    print(f"{headers[0]:<6} {headers[1]:<12} {headers[2]:<12} {headers[3]:<12} {headers[4]:<12}")
    print("-" * 60)

    results = []

    for i in range(REPEATS):
        # Benchmark normal string +=
        t_str = timeit.timeit(
            'result = ""\nfor s in test_strings: result += s',
            globals={'test_strings': test_strings},
            number=N
        ) * 1000

        # Benchmark ExplicitList
        t_explicit = timeit.timeit(
            'obj = ExplicitList()\nfor s in test_strings: obj.emit(s)',
            globals={'test_strings': test_strings, 'ExplicitList': ExplicitList},
            number=N
        ) * 1000

        # Benchmark __add__ version
        t_add = timeit.timeit(
            'obj = OutputStringAdd()\nfor s in test_strings: obj += s',
            globals={'test_strings': test_strings, 'OutputStringAdd': OutputStringAdd},
            number=N
        ) * 1000

        # Benchmark __iadd__ version
        t_iadd = timeit.timeit(
            'obj = OutputStringIAdd()\nfor s in test_strings: obj += s',
            globals={'test_strings': test_strings, 'OutputStringIAdd': OutputStringIAdd},
            number=N
        ) * 1000

        results.append((t_str, t_explicit, t_add, t_iadd))
        print(f"{i+1:<6} {t_str:<12.2f} {t_explicit:<12.2f} {t_add:<12.2f} {t_iadd:<12.2f}")

    # Calculate averages
    avg_str = sum(r[0] for r in results) / REPEATS
    avg_explicit = sum(r[1] for r in results) / REPEATS
    avg_add = sum(r[2] for r in results) / REPEATS
    avg_iadd = sum(r[3] for r in results) / REPEATS

    print("-" * 60)
    print(f"{'Avg':<6} {avg_str:<12.2f} {avg_explicit:<12.2f} {avg_add:<12.2f} {avg_iadd:<12.2f}")

    print()
    print("Summary:")
    # Calculate and print comparisons
    for name, time in [("ExplicitList", avg_explicit),
                      ("__add__", avg_add),
                      ("__iadd__", avg_iadd)]:
        is_faster, percentage = calculate_comparison(avg_str, time)
        if is_faster:
            print(f"{name:<12} : {percentage:.2f}% faster than str+=")
        else:
            print(f"{name:<12} : {percentage:.2f}% slower than str+=")


if __name__ == "__main__":
    benchmark()
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Jonathan Corbet 2 months, 4 weeks ago
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:

> With that, I would just drop this patch, as the performance is
> almost identical, and using "emit()" instead of "+=" IMO makes
> the code less clear.

I've dropped the patch - for now - but I really disagree with the latter
part of that sentence.  It is far better, IMO, to encapsulate the
construction of our output rather than spreading vast numbers of direct
string concatenations throughout the code.  So this one will likely be
back in a different form :)

Thanks,

jon
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Thu, 10 Jul 2025 17:30:20 -0600
Jonathan Corbet <corbet@lwn.net> escreveu:

> Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:
> 
> > With that, I would just drop this patch, as the performance is
> > almost identical, and using "emit()" instead of "+=" IMO makes
> > the code less clear.  
> 
> I've dropped the patch - for now - but I really disagree with the latter
> part of that sentence.  It is far better, IMO, to encapsulate the
> construction of our output rather than spreading vast numbers of direct
> string concatenations throughout the code.  So this one will likely be
> back in a different form :)

The main concern was related to performance penalty - as based on
the latest test results, Pyhon currently handles very poorly list
concat (30% to 200% slower at the latest test results).

Yet, at least for me with my C-trained brain parsing, I find "=+" a
lot easier to understand than some_function().

Btw, IMHO Python is not particularly great with names for concat/accumulate
commands. For list, it is append(), for set it is add(). Yet, "+=" is almost
universal (from standard types, only sets don't accept it, using, 
instead, "|=", which kind of makes sense).

Adding a function naming emit() - at least for me - requires an extra brain 
processing time to remember that emit is actually a function that doesn't
produce any emission: it just stores data for a future output - that may 
even not happen if one calls the script with "--none".

Thanks,
Mauro
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Jonathan Corbet 2 months, 4 weeks ago
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:

> Em Thu, 10 Jul 2025 17:30:20 -0600
> Jonathan Corbet <corbet@lwn.net> escreveu:
>
>> Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:
>> 
>> > With that, I would just drop this patch, as the performance is
>> > almost identical, and using "emit()" instead of "+=" IMO makes
>> > the code less clear.  
>> 
>> I've dropped the patch - for now - but I really disagree with the latter
>> part of that sentence.  It is far better, IMO, to encapsulate the
>> construction of our output rather than spreading vast numbers of direct
>> string concatenations throughout the code.  So this one will likely be
>> back in a different form :)
>
> The main concern was related to performance penalty - as based on
> the latest test results, Pyhon currently handles very poorly list
> concat (30% to 200% slower at the latest test results).

Yes, I understood that part

> Yet, at least for me with my C-trained brain parsing, I find "=+" a
> lot easier to understand than some_function().
>
> Btw, IMHO Python is not particularly great with names for concat/accumulate
> commands. For list, it is append(), for set it is add(). Yet, "+=" is almost
> universal (from standard types, only sets don't accept it, using, 
> instead, "|=", which kind of makes sense).
>
> Adding a function naming emit() - at least for me - requires an extra brain 
> processing time to remember that emit is actually a function that doesn't
> produce any emission: it just stores data for a future output - that may 
> even not happen if one calls the script with "--none".

OK, I'll ponder on a different name :)

Perhaps the new not_emit() could even be aware of --none and just drop
the data on the floor.

Thanks,

jon
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Fri, 11 Jul 2025 06:49:26 -0600
Jonathan Corbet <corbet@lwn.net> escreveu:

> Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:
> 
> > Em Thu, 10 Jul 2025 17:30:20 -0600
> > Jonathan Corbet <corbet@lwn.net> escreveu:
> >  
> >> Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:
> >>   
> >> > With that, I would just drop this patch, as the performance is
> >> > almost identical, and using "emit()" instead of "+=" IMO makes
> >> > the code less clear.    
> >> 
> >> I've dropped the patch - for now - but I really disagree with the latter
> >> part of that sentence.  It is far better, IMO, to encapsulate the
> >> construction of our output rather than spreading vast numbers of direct
> >> string concatenations throughout the code.  So this one will likely be
> >> back in a different form :)  
> >
> > The main concern was related to performance penalty - as based on
> > the latest test results, Pyhon currently handles very poorly list
> > concat (30% to 200% slower at the latest test results).  
> 
> Yes, I understood that part
> 
> > Yet, at least for me with my C-trained brain parsing, I find "=+" a
> > lot easier to understand than some_function().
> >
> > Btw, IMHO Python is not particularly great with names for concat/accumulate
> > commands. For list, it is append(), for set it is add(). Yet, "+=" is almost
> > universal (from standard types, only sets don't accept it, using, 
> > instead, "|=", which kind of makes sense).
> >
> > Adding a function naming emit() - at least for me - requires an extra brain 
> > processing time to remember that emit is actually a function that doesn't
> > produce any emission: it just stores data for a future output - that may 
> > even not happen if one calls the script with "--none".  
> 
> OK, I'll ponder on a different name :)

I'm fine with that.

> Perhaps the new not_emit() could even be aware of --none and just drop
> the data on the floor.

The code already does that on a much more optimized way. This
is actually one of the improvements over the Perl version: we
don't need to implement anything special for none.

When --none is passed, the code sets out_style = OutputFormat(), 
which is pretty much an abstract class that doesn't do any output 
at all, and from where the ManOutput and Restformat classes
are inherited.

It only does two things:

- Applying filters, in order to filter-out warnings from things
  according with --import/--internal/--function arguments;

- print warnings for symbols after filtering them, with:

    def out_warnings(self, args):
        """
        Output warnings for identifiers that will be displayed.
        """

        warnings = args.get('warnings', [])

        for log_msg in warnings:
            self.config.warning(log_msg)

So, there's no emit()/no_emit()/print()... there. All output
types do nothing:

    # Virtual methods to be overridden by inherited classes
    # At the base class, those do nothing.
    def out_doc(self, fname, name, args):
        """Outputs a DOC block"""

    def out_function(self, fname, name, args):
        """Outputs a function"""

    def out_enum(self, fname, name, args):
        """Outputs an enum"""

    def out_typedef(self, fname, name, args):
        """Outputs a typedef"""

    def out_struct(self, fname, name, args):
        """Outputs a struct"""

Regards,
Mauro
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Jonathan Corbet 2 months, 4 weeks ago
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:

>> Perhaps the new not_emit() could even be aware of --none and just drop
>> the data on the floor.
>
> The code already does that on a much more optimized way. This
> is actually one of the improvements over the Perl version: we
> don't need to implement anything special for none.

So your comment on emit() being mis-named for the --none case didn't
actually apply... :)

jon
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Thu, 10 Jul 2025 10:19:31 +0200
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:

> Em Thu, 10 Jul 2025 09:13:52 +0200
> Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:
> 

> Heh, on those times where LLM can quickly code trivial things for us,
> I actually decided to test 3 different variants:
> 
> - using string +=
> - using list append
> - using __add__
> - using __iadd__

Manually reorganized the LLM-generated code, in order to get more
precise results. Script enclosed at the end.

  $ for i in python3.9 python3.13 python3.13t; do echo "  $i:"; $i /tmp/bench.py 100000 10 1; $i /tmp/bench.py 1000 1000 1; done
  python3.9:
    10 strings in a loop with 100000 interactions, repeating 24 times
        str +=       : time: 25.21
        list join    : time: 72.65: 188.18% slower than str +=
        __add__      : time: 71.82: 184.88% slower than str +=
        __iadd__     : time: 67.84: 169.09% slower than str +=
    1000 strings in a loop with 1000 interactions, repeating 24 times
        str +=       : time: 24.29
        list join    : time: 58.76: 141.88% slower than str +=
        __add__      : time: 58.68: 141.54% slower than str +=
        __iadd__     : time: 55.48: 128.37% slower than str +=
  python3.13:
    10 strings in a loop with 100000 interactions, repeating 24 times
        str +=       : time: 28.01
        list join    : time: 32.46: 15.91% slower than str +=
        __add__      : time: 52.56: 87.66% slower than str +=
        __iadd__     : time: 58.69: 109.55% slower than str +=
    1000 strings in a loop with 1000 interactions, repeating 24 times
        str +=       : time: 22.03
        list join    : time: 23.38: 6.12% slower than str +=
        __add__      : time: 44.25: 100.86% slower than str +=
        __iadd__     : time: 40.70: 84.74% slower than str +=
  python3.13t:
    10 strings in a loop with 100000 interactions, repeating 24 times
        str +=       : time: 25.65
        list join    : time: 74.95: 192.18% slower than str +=
        __add__      : time: 83.04: 223.71% slower than str +=
        __iadd__     : time: 79.07: 208.23% slower than str +=
    1000 strings in a loop with 1000 interactions, repeating 24 times
        str +=       : time: 57.39
        list join    : time: 62.31: 8.58% slower than str +=
        __add__      : time: 70.65: 23.10% slower than str +=
        __iadd__     : time: 68.67: 19.65% slower than str +=

From the above:

- It is not worth applying patch 12/12 as it makes the code slower;
- Python 3.13t (no-GIL version) had very bad results. It seems it
  still requires optimization;
- Python 3.9 is a lot worse (140% to 190%) when using list append;
- when there are not many concats, Python 3.13 is about 15% slower
  with lists than concat strings. It only approaches str concat
  when the number of concats is high.

With the above, clearly str += is faster than list append.

So, except if I did something wrong on this benchmark script, please
don't apply patch 12/12.

Regards,
Mauro

---

Benchmark code:

#!/usr/bin/env python3

import argparse
import time
import sys

def benchmark_str_concat(test_strings, n_ops):
    start = time.time()
    for _ in range(n_ops):
        result = ""
        for s in test_strings:
            result += s
    return (time.time() - start) * 1000

def benchmark_explicit_list(test_strings, n_ops):
    class ExplicitList:
        def __init__(self):
            self._output = []

        def emit(self, text):
            self._output.append(text)

        def output(self):
            return ''.join(self._output)

    start = time.time()
    for _ in range(n_ops):
        obj = ExplicitList()
        for s in test_strings:
            obj.emit(s)
    return (time.time() - start) * 1000

def benchmark_add_overload(test_strings, n_ops):
    class OutputStringAdd:
        def __init__(self):
            self._output = []

        def __add__(self, text):
            self._output.append(text)
            return self

        def __str__(self):
            return ''.join(self._output)

    start = time.time()
    for _ in range(n_ops):
        obj = OutputStringAdd()
        for s in test_strings:
            obj += s
    return (time.time() - start) * 1000

def benchmark_iadd_overload(test_strings, n_ops):
    class OutputStringIAdd:
        def __init__(self):
            self._output = []

        def __iadd__(self, text):
            self._output.append(text)
            return self

        def __str__(self):
            return ''.join(self._output)

    start = time.time()
    for _ in range(n_ops):
        obj = OutputStringIAdd()
        for s in test_strings:
            obj += s
    return (time.time() - start) * 1000

def calculate_comparison(base_time, compare_time):
    if compare_time < base_time:
        return (True, (1 - compare_time/base_time)*100)
    return (False, (compare_time/base_time - 1)*100)

def benchmark(num_reps, strings_per_run, repeats, detail):
    test_strings = [f"string_{i:03d}" for i in range(strings_per_run)]

    # Create benchmark execution order list
    benchmarks = [
        ("str +=", benchmark_str_concat),
        ("list join", benchmark_explicit_list),
        ("__add__", benchmark_add_overload),
        ("__iadd__", benchmark_iadd_overload)
    ]

    # Use all possible permutations of benchmark order to reduce any
    # noise due to CPU caches
    all_orders = [
        (0, 1, 2, 3), (0, 1, 3, 2), (0, 2, 1, 3), (0, 2, 3, 1),
        (0, 3, 1, 2), (0, 3, 2, 1), (1, 0, 2, 3), (1, 0, 3, 2),
        (1, 2, 0, 3), (1, 2, 3, 0), (1, 3, 0, 2), (1, 3, 2, 0),
        (2, 0, 1, 3), (2, 0, 3, 1), (2, 1, 0, 3), (2, 1, 3, 0),
        (2, 3, 0, 1), (2, 3, 1, 0), (3, 0, 1, 2), (3, 0, 2, 1),
        (3, 1, 0, 2), (3, 1, 2, 0), (3, 2, 0, 1), (3, 2, 1, 0)
    ]

    results = {}
    for name, _ in benchmarks:
        results[name] = 0

    # Warm-up phase to reduce caching issues
    for name, fn in benchmarks:
        fn(test_strings, 1)

    n_repeats = len(all_orders) * repeats
    print(f"    {strings_per_run} strings in a loop with {num_reps} interactions, repeating {n_repeats} times")

    # Actual benchmark starts here
    i = 0
    if detail:
        headers = ['Run'] + [name for name, _ in benchmarks]
        print()
        print(f"\t{headers[0]:<6} {headers[1]:<12} {headers[2]:<12} {headers[3]:<12} {headers[4]:<12}")
        print("\t" + "-" * 60)
    for _ in range(repeats):
        # Shuffle execution order each run
        for order in all_orders:
            run_results = {}
            for idx in order:
                name, func = benchmarks[idx]
                run_results[name] = func(test_strings, num_reps)
                results[name] += run_results[name]

            if detail:
                # Output results in consistent order
                print(f"\t{i+1:<6}", end=" ")
                for name, _ in benchmarks:
                    print(f"{run_results[name]:<12.2f}", end=" ")
                print()

            i += 1

    avg_results = {}
    for name, _ in benchmarks:
        avg_results[name] = results[name] / repeats / len(all_orders)

    if detail:
        print("\t" + "-" * 60)
        print(f"\t      ", end=" ")
        for name, _ in benchmarks:
            print(f"{avg_results[name]:<12.2f}", end=" ")
        print()
        print()

    ref = benchmarks.pop(0)

    print(f"\t{ref[0]:<12} : time: {avg_results[ref[0]]:3.2f}")
    for name, _ in benchmarks:
        is_faster, percentage = calculate_comparison(avg_results[ref[0]], avg_results[name])
        direction = "faster" if is_faster else "slower"
        print(f"\t{name:<12} : time: {avg_results[name]:3.2f}: {percentage:3.2f}% {direction} than {ref[0]}")



if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--detail', action='store_true',
                       help='Enable detailed output')

    args, remaining = parser.parse_known_args()

    # Then handle the positional arguments manually
    if len(remaining) != 3:
        print(f"Usage: {sys.argv[0]} [-d] <num_repetitions> <strings_per_op> <repeats>")
        sys.exit(1)

    num_reps = int(remaining[0])
    strings_per_op = int(remaining[1])
    repeats = int(remaining[2])

    num_reps = int(sys.argv[1])
    strings_per_op = int(sys.argv[2])
    repeats = int(sys.argv[3])

    benchmark(num_reps, strings_per_op, repeats, args.detail)
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Thu, 10 Jul 2025 12:10:33 +0200
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:

> From the above:
> 
> - It is not worth applying patch 12/12 as it makes the code slower;
> - Python 3.13t (no-GIL version) had very bad results. It seems it
>   still requires optimization;
> - Python 3.9 is a lot worse (140% to 190%) when using list append;
> - when there are not many concats, Python 3.13 is about 15% slower
>   with lists than concat strings. It only approaches str concat
>   when the number of concats is high.
> 
> With the above, clearly str += is faster than list append.
> 
> So, except if I did something wrong on this benchmark script, please
> don't apply patch 12/12.

And I did: I forgot the final line at the concat code to get the
result as strings.

For explicit list:
	result = obj.output()

For implicit ones:
	result = str(obj)

Yet, the conclusion is similar. With Python 3.13:

    $ for i in python3.13; do for j in 1 10 100 1000; do $i /tmp/bench.py $((1000000/$j)) $j 1; done; done
    1 strings in a loop with 1000000 interactions, repeating 24 times
        str +=       : time: 41.42
        list join    : time: 127.33: 207.42% slower than str +=
    10 strings in a loop with 100000 interactions, repeating 24 times
        str +=       : time: 27.15
        list join    : time: 39.19: 44.36% slower than str +=
    100 strings in a loop with 10000 interactions, repeating 24 times
        str +=       : time: 24.84
        list join    : time: 30.70: 23.57% slower than str +=
    1000 strings in a loop with 1000 interactions, repeating 24 times
        str +=       : time: 21.84
        list join    : time: 27.85: 27.50% slower than str +=

Explict list concat was between ~30% to ~200% worse than str concat.


Thanks,
Mauro

---

#!/usr/bin/env python3

import argparse
import time
import sys

def benchmark_str_concat(test_strings, n_ops):
    start = time.time()
    for _ in range(n_ops):
        result = ""
        for s in test_strings:
            result += s
    return (time.time() - start) * 1000

def benchmark_explicit_list(test_strings, n_ops):
    class ExplicitList:
        def __init__(self):
            self._output = []

        def emit(self, text):
            self._output.append(text)

        def output(self):
            return ''.join(self._output)

    start = time.time()
    for _ in range(n_ops):
        obj = ExplicitList()
        for s in test_strings:
            obj.emit(s)

        result = obj.output()

    return (time.time() - start) * 1000

def benchmark_add_overload(test_strings, n_ops):
    class OutputStringAdd:
        def __init__(self):
            self._output = []

        def __add__(self, text):
            self._output.append(text)
            return self

        def __str__(self):
            return ''.join(self._output)

    start = time.time()
    for _ in range(n_ops):
        obj = OutputStringAdd()
        for s in test_strings:
            obj += s

        result = str(obj)

    return (time.time() - start) * 1000

def benchmark_iadd_overload(test_strings, n_ops):
    class OutputStringIAdd:
        def __init__(self):
            self._output = []

        def __iadd__(self, text):
            self._output.append(text)
            return self

        def __str__(self):
            return ''.join(self._output)

    start = time.time()
    for _ in range(n_ops):
        obj = OutputStringIAdd()
        for s in test_strings:
            obj += s
        result = str(obj)

    return (time.time() - start) * 1000

def calculate_comparison(base_time, compare_time):
    if compare_time < base_time:
        return (True, (1 - compare_time/base_time)*100)
    return (False, (compare_time/base_time - 1)*100)

def benchmark(num_reps, strings_per_run, repeats, detail):
    test_strings = [f"string_{i:03d}" for i in range(strings_per_run)]

    # Create benchmark execution order list
    benchmarks = [
        ("str +=", benchmark_str_concat),
        ("list join", benchmark_explicit_list),
        ("__add__", benchmark_add_overload),
        ("__iadd__", benchmark_iadd_overload)
    ]

    # Use all possible permutations of benchmark order to reduce any
    # noise due to CPU caches
    all_orders = [
        (0, 1, 2, 3), (0, 1, 3, 2), (0, 2, 1, 3), (0, 2, 3, 1),
        (0, 3, 1, 2), (0, 3, 2, 1), (1, 0, 2, 3), (1, 0, 3, 2),
        (1, 2, 0, 3), (1, 2, 3, 0), (1, 3, 0, 2), (1, 3, 2, 0),
        (2, 0, 1, 3), (2, 0, 3, 1), (2, 1, 0, 3), (2, 1, 3, 0),
        (2, 3, 0, 1), (2, 3, 1, 0), (3, 0, 1, 2), (3, 0, 2, 1),
        (3, 1, 0, 2), (3, 1, 2, 0), (3, 2, 0, 1), (3, 2, 1, 0)
    ]

    results = {}
    for name, _ in benchmarks:
        results[name] = 0

    # Warm-up phase to reduce caching issues
    for name, fn in benchmarks:
        fn(test_strings, 1)

    n_repeats = len(all_orders) * repeats
    print(f"    {strings_per_run} strings in a loop with {num_reps} interactions, repeating {n_repeats} times")

    # Actual benchmark starts here
    i = 0
    if detail:
        headers = ['Run'] + [name for name, _ in benchmarks]
        print()
        print(f"\t{headers[0]:<6} {headers[1]:<12} {headers[2]:<12} {headers[3]:<12} {headers[4]:<12}")
        print("\t" + "-" * 60)
    for _ in range(repeats):
        # Shuffle execution order each run
        for order in all_orders:
            run_results = {}
            for idx in order:
                name, func = benchmarks[idx]
                run_results[name] = func(test_strings, num_reps)
                results[name] += run_results[name]

            if detail:
                # Output results in consistent order
                print(f"\t{i+1:<6}", end=" ")
                for name, _ in benchmarks:
                    print(f"{run_results[name]:<12.2f}", end=" ")
                print()

            i += 1

    avg_results = {}
    for name, _ in benchmarks:
        avg_results[name] = results[name] / repeats / len(all_orders)

    if detail:
        print("\t" + "-" * 60)
        print(f"\t      ", end=" ")
        for name, _ in benchmarks:
            print(f"{avg_results[name]:<12.2f}", end=" ")
        print()
        print()

    ref = benchmarks.pop(0)

    print(f"\t{ref[0]:<12} : time: {avg_results[ref[0]]:3.2f}")
    for name, _ in benchmarks:
        is_faster, percentage = calculate_comparison(avg_results[ref[0]], avg_results[name])
        direction = "faster" if is_faster else "slower"
        print(f"\t{name:<12} : time: {avg_results[name]:3.2f}: {percentage:3.2f}% {direction} than {ref[0]}")



if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--detail', action='store_true',
                       help='Enable detailed output')

    args, remaining = parser.parse_known_args()

    # Then handle the positional arguments manually
    if len(remaining) != 3:
        print(f"Usage: {sys.argv[0]} [-d] <num_repetitions> <strings_per_op> <repeats>")
        sys.exit(1)

    num_reps = int(remaining[0])
    strings_per_op = int(remaining[1])
    repeats = int(remaining[2])

    num_reps = int(sys.argv[1])
    strings_per_op = int(sys.argv[2])
    repeats = int(sys.argv[3])

    benchmark(num_reps, strings_per_op, repeats, args.detail)
Re: [PATCH 12/12] docs: kdoc: Improve the output text accumulation
Posted by Mauro Carvalho Chehab 2 months, 4 weeks ago
Em Thu, 10 Jul 2025 12:31:55 +0200
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:

> Em Thu, 10 Jul 2025 12:10:33 +0200
> Mauro Carvalho Chehab <mchehab+huawei@kernel.org> escreveu:
> 
> > From the above:
> > 
> > - It is not worth applying patch 12/12 as it makes the code slower;
> > - Python 3.13t (no-GIL version) had very bad results. It seems it
> >   still requires optimization;
> > - Python 3.9 is a lot worse (140% to 190%) when using list append;
> > - when there are not many concats, Python 3.13 is about 15% slower
> >   with lists than concat strings. It only approaches str concat
> >   when the number of concats is high.
> > 
> > With the above, clearly str += is faster than list append.
> > 
> > So, except if I did something wrong on this benchmark script, please
> > don't apply patch 12/12.  
> 
> And I did: I forgot the final line at the concat code to get the
> result as strings.
> 
> For explicit list:
> 	result = obj.output()
> 
> For implicit ones:
> 	result = str(obj)
> 
> Yet, the conclusion is similar. With Python 3.13:
> 
>     $ for i in python3.13; do for j in 1 10 100 1000; do $i /tmp/bench.py $((1000000/$j)) $j 1; done; done
>     1 strings in a loop with 1000000 interactions, repeating 24 times
>         str +=       : time: 41.42
>         list join    : time: 127.33: 207.42% slower than str +=
>     10 strings in a loop with 100000 interactions, repeating 24 times
>         str +=       : time: 27.15
>         list join    : time: 39.19: 44.36% slower than str +=
>     100 strings in a loop with 10000 interactions, repeating 24 times
>         str +=       : time: 24.84
>         list join    : time: 30.70: 23.57% slower than str +=
>     1000 strings in a loop with 1000 interactions, repeating 24 times
>         str +=       : time: 21.84
>         list join    : time: 27.85: 27.50% slower than str +=
> 
> Explict list concat was between ~30% to ~200% worse than str concat.

Looking for an explanation, PEP 509 and PEP 393 did Short String Optimization
and Inline Caching. This was applied Python 3.6, and Python 3.13 came with
extra string optimizations.

On the other hand, lists do more memory allocations, have some logic to
extend list growth and has an extra concat loop.

With that, contrary to popular belief, it sounds that str concat are
nowadays faster.

Thanks,
Mauro