This is an automated email from the ASF dual-hosted git repository.

HeartSaVioR pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 181acbd8db57 [SPARK-57760][SS] Make small optimizations to 
StatefulProcessorApiClient
181acbd8db57 is described below

commit 181acbd8db5738ff37509357c1b4d8e82396db27
Author: Sagar Mittal <[email protected]>
AuthorDate: Thu Jul 9 13:35:59 2026 +0900

    [SPARK-57760][SS] Make small optimizations to StatefulProcessorApiClient
    
    ### What changes were proposed in this pull request?
    
    Make two small optimizations to StatefulProcessorApiClient:
    1. Call PickleSerializer() instead of using the default CPickleSerializer 
(which is CloudPickleSerializer). We don't need the latter since this path does 
not deal with code objects.
    2. Micro-optimize state value normalization: add a fast-path for 
primitives, prefer `map` to  generator comprehensions, and move the numpy 
import and function definition to the top level so it is done once
    
    #### Benchmarks
    
    This is a [microbenchmark for 
`_serialize_to_bytes`](https://gist.github.com/funrollloops/62bf5fae1654910b63a4539e1181db91):
    
    ##### Before
    ```
    --- single-field tuples ---
      python int     → LongType                         p50=  4.11µs  p95=  
4.21µs  p99=  6.49µs
      python float   → DoubleType                       p50=  4.10µs  p95=  
4.20µs  p99=  4.96µs
      np.float64     → DoubleType                       p50=  4.42µs  p95=  
4.57µs  p99=  6.46µs
      np.datetime64  → Timestamp                        p50=  7.32µs  p95=  
7.67µs  p99= 11.60µs
      pd.Timestamp   → Timestamp                        p50=  7.53µs  p95=  
7.76µs  p99= 12.34µs
    
    --- wider tuples ---
      10× python float → 10× DoubleType                 p50=  7.80µs  p95=  
7.94µs  p99= 12.37µs
      10× np.float64   → 10× DoubleType                 p50=  9.01µs  p95=  
9.23µs  p99= 14.88µs
      mixed (np.f64, np.i64, str, pd.Ts)                p50=  9.93µs  p95= 
10.26µs  p99= 17.66µs
    
    ```
    
    ##### After
    ```
    --- single-field tuples ---
      python int     → LongType                         p50=  1.17µs  p95=  
1.19µs  p99=  1.22µs
      python float   → DoubleType                       p50=  1.18µs  p95=  
1.19µs  p99=  1.22µs
      np.float64     → DoubleType                       p50=  1.92µs  p95=  
1.98µs  p99=  2.01µs
      np.datetime64  → Timestamp                        p50=  4.59µs  p95=  
4.71µs  p99=  4.78µs
      pd.Timestamp   → Timestamp                        p50=  5.07µs  p95=  
5.17µs  p99=  5.24µs
    
    --- wider tuples ---
      10× python float → 10× DoubleType                 p50=  2.19µs  p95=  
2.23µs  p99=  2.26µs
      10× np.float64   → 10× DoubleType                 p50=  7.72µs  p95=  
7.82µs  p99=  7.89µs
      mixed (np.f64, np.i64, str, pd.Ts)                p50=  7.34µs  p95=  
7.46µs  p99=  7.58µs
    ```
    
    ### Why are the changes needed?
    
    Together these changes improve transform with state on a simple 
rolling-window style benchmark by ~10%.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Existing unit tests.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    No, but Claude was consulted in the process of producing this PR.
    
    Closes #56786 from funrollloops/tws-opt-1.
    
    Lead-authored-by: Sagar Mittal <[email protected]>
    Co-authored-by: Sagar Mittal <[email protected]>
    Signed-off-by: Jungtaek Lim <[email protected]>
    (cherry picked from commit 0af85eee81716bb2095c8ee2af39b77e7cca9a8f)
    Signed-off-by: Jungtaek Lim <[email protected]>
---
 .../sql/streaming/stateful_processor_api_client.py | 67 ++++++++++++----------
 1 file changed, 36 insertions(+), 31 deletions(-)

diff --git a/python/pyspark/sql/streaming/stateful_processor_api_client.py 
b/python/pyspark/sql/streaming/stateful_processor_api_client.py
index eab91e0c3f84..166ac52fa6d8 100644
--- a/python/pyspark/sql/streaming/stateful_processor_api_client.py
+++ b/python/pyspark/sql/streaming/stateful_processor_api_client.py
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+from datetime import datetime
 from enum import Enum
 import json
 import os
@@ -27,12 +28,43 @@ from pyspark.sql.types import (
     Row,
 )
 from pyspark.sql.pandas.types import convert_pandas_using_numpy_type
-from pyspark.serializers import CPickleSerializer
+from pyspark.serializers import PickleSerializer
 from pyspark.errors import PySparkRuntimeError
 import uuid
 
 __all__ = ["StatefulProcessorApiClient", "StatefulProcessorHandleState"]
 
+try:
+    import numpy as np
+
+    has_numpy = True
+    SCALAR_TYPES = (bool, int, float, str, bytes, datetime, type(None))
+
+    def _normalize_state_value(v: Any) -> Any:
+        if type(v) in SCALAR_TYPES:  # Fast path for common scalar values.
+            return v
+        # Convert NumPy scalar values to Python primitive values.
+        if isinstance(v, np.generic):
+            return v.tolist()
+        # Named tuples (collections.namedtuple or typing.NamedTuple) and Row 
both
+        # require positional arguments and cannot be instantiated with a 
generator expression.
+        if isinstance(v, Row) or (isinstance(v, tuple) and hasattr(v, 
"_fields")):
+            return type(v)(*map(_normalize_state_value, v))
+        # List / tuple: recursively normalize each element.
+        if isinstance(v, (list, tuple)):
+            return type(v)(map(_normalize_state_value, v))
+        # Dict: normalize both keys and values.
+        if isinstance(v, dict):
+            return {_normalize_state_value(k): _normalize_state_value(val) for 
k, val in v.items()}
+        # Address a couple of pandas dtypes too.
+        if hasattr(v, "to_pytimedelta"):
+            return v.to_pytimedelta()
+        if hasattr(v, "to_pydatetime"):
+            return v.to_pydatetime()
+        return v
+except ImportError:
+    has_numpy = False
+
 
 class StatefulProcessorHandleState(Enum):
     PRE_INIT = 0
@@ -74,7 +106,7 @@ class StatefulProcessorApiClient:
         else:
             self.handle_state = StatefulProcessorHandleState.CREATED
         self.utf8_deserializer = UTF8Deserializer()
-        self.pickleSer = CPickleSerializer()
+        self.pickleSer = PickleSerializer()
         self.serializer = ArrowStreamSerializer()
         # Dictionaries to store the mapping between iterator id and a tuple of 
data batch
         # and the index of the last row that was read.
@@ -488,35 +520,8 @@ class StatefulProcessorApiClient:
         return self.utf8_deserializer.loads(self.sockfile)
 
     def _serialize_to_bytes(self, schema: StructType, data: Tuple) -> bytes:
-        from pyspark.testing.utils import have_numpy
-
-        if have_numpy:
-            import numpy as np
-
-            def normalize_value(v: Any) -> Any:
-                # Convert NumPy types to Python primitive types.
-                if isinstance(v, np.generic):
-                    return v.tolist()
-                # Named tuples (collections.namedtuple or typing.NamedTuple) 
and Row both
-                # require positional arguments and cannot be instantiated
-                # with a generator expression.
-                if isinstance(v, Row) or (isinstance(v, tuple) and hasattr(v, 
"_fields")):
-                    return type(v)(*[normalize_value(e) for e in v])
-                # List / tuple: recursively normalize each element
-                if isinstance(v, (list, tuple)):
-                    return type(v)(normalize_value(e) for e in v)
-                # Dict: normalize both keys and values
-                if isinstance(v, dict):
-                    return {normalize_value(k): normalize_value(val) for k, 
val in v.items()}
-                # Address a couple of pandas dtypes too.
-                elif hasattr(v, "to_pytimedelta"):
-                    return v.to_pytimedelta()
-                elif hasattr(v, "to_pydatetime"):
-                    return v.to_pydatetime()
-                else:
-                    return v
-
-            converted = tuple(normalize_value(v) for v in data)
+        if has_numpy:
+            converted = tuple(map(_normalize_state_value, data))
         else:
             converted = data
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to