github.com/apache/arrow/go/v14@v14.0.2/arrow/compute/exec/kernel.go (about) 1 // Licensed to the Apache Software Foundation (ASF) under one 2 // or more contributor license agreements. See the NOTICE file 3 // distributed with this work for additional information 4 // regarding copyright ownership. The ASF licenses this file 5 // to you under the Apache License, Version 2.0 (the 6 // "License"); you may not use this file except in compliance 7 // with the License. You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 //go:build go1.18 18 19 package exec 20 21 import ( 22 "context" 23 "fmt" 24 "hash/maphash" 25 "strings" 26 27 "github.com/apache/arrow/go/v14/arrow" 28 "github.com/apache/arrow/go/v14/arrow/bitutil" 29 "github.com/apache/arrow/go/v14/arrow/internal/debug" 30 "github.com/apache/arrow/go/v14/arrow/memory" 31 "golang.org/x/exp/slices" 32 ) 33 34 var hashSeed = maphash.MakeSeed() 35 36 type ctxAllocKey struct{} 37 38 // WithAllocator returns a new context with the provided allocator 39 // embedded into the context. 40 func WithAllocator(ctx context.Context, mem memory.Allocator) context.Context { 41 return context.WithValue(ctx, ctxAllocKey{}, mem) 42 } 43 44 // GetAllocator retrieves the allocator from the context, or returns 45 // memory.DefaultAllocator if there was no allocator in the provided 46 // context. 47 func GetAllocator(ctx context.Context) memory.Allocator { 48 mem, ok := ctx.Value(ctxAllocKey{}).(memory.Allocator) 49 if !ok { 50 return memory.DefaultAllocator 51 } 52 return mem 53 } 54 55 // Kernel defines the minimum interface required for the basic execution 56 // kernel. It will grow as the implementation requires. 57 type Kernel interface { 58 GetInitFn() KernelInitFn 59 GetSig() *KernelSignature 60 } 61 62 // NonAggKernel builds on the base Kernel interface for 63 // non aggregate execution kernels. Specifically this will 64 // represent Scalar and Vector kernels. 65 type NonAggKernel interface { 66 Kernel 67 Exec(*KernelCtx, *ExecSpan, *ExecResult) error 68 GetNullHandling() NullHandling 69 GetMemAlloc() MemAlloc 70 CanFillSlices() bool 71 } 72 73 // KernelCtx is a small struct holding the context for a kernel execution 74 // consisting of a pointer to the kernel, initialized state (if needed) 75 // and the context for this execution. 76 type KernelCtx struct { 77 Ctx context.Context 78 Kernel Kernel 79 State KernelState 80 } 81 82 func (k *KernelCtx) Allocate(bufsize int) *memory.Buffer { 83 buf := memory.NewResizableBuffer(GetAllocator(k.Ctx)) 84 buf.Resize(bufsize) 85 return buf 86 } 87 88 func (k *KernelCtx) AllocateBitmap(nbits int64) *memory.Buffer { 89 nbytes := bitutil.BytesForBits(nbits) 90 return k.Allocate(int(nbytes)) 91 } 92 93 // TypeMatcher define an interface for matching Input or Output types 94 // for execution kernels. There are multiple implementations of this 95 // interface provided by this package. 96 type TypeMatcher interface { 97 fmt.Stringer 98 Matches(typ arrow.DataType) bool 99 Equals(other TypeMatcher) bool 100 } 101 102 type sameTypeIDMatcher struct { 103 accepted arrow.Type 104 } 105 106 func (s sameTypeIDMatcher) Matches(typ arrow.DataType) bool { return s.accepted == typ.ID() } 107 func (s sameTypeIDMatcher) Equals(other TypeMatcher) bool { 108 if s == other { 109 return true 110 } 111 112 o, ok := other.(*sameTypeIDMatcher) 113 if !ok { 114 return false 115 } 116 117 return s.accepted == o.accepted 118 } 119 120 func (s sameTypeIDMatcher) String() string { 121 return "Type::" + s.accepted.String() 122 } 123 124 // SameTypeID returns a type matcher which will match 125 // any DataType that uses the same arrow.Type ID as the one 126 // passed in here. 127 func SameTypeID(id arrow.Type) TypeMatcher { return &sameTypeIDMatcher{id} } 128 129 type timeUnitMatcher struct { 130 id arrow.Type 131 unit arrow.TimeUnit 132 } 133 134 func (s timeUnitMatcher) Matches(typ arrow.DataType) bool { 135 if typ.ID() != s.id { 136 return false 137 } 138 return s.unit == typ.(arrow.TemporalWithUnit).TimeUnit() 139 } 140 141 func (s timeUnitMatcher) String() string { 142 return strings.ToLower(s.id.String()) + "(" + s.unit.String() + ")" 143 } 144 145 func (s *timeUnitMatcher) Equals(other TypeMatcher) bool { 146 if s == other { 147 return true 148 } 149 150 o, ok := other.(*timeUnitMatcher) 151 if !ok { 152 return false 153 } 154 return o.id == s.id && o.unit == s.unit 155 } 156 157 // TimestampTypeUnit returns a TypeMatcher that will match only 158 // a Timestamp datatype with the specified TimeUnit. 159 func TimestampTypeUnit(unit arrow.TimeUnit) TypeMatcher { 160 return &timeUnitMatcher{arrow.TIMESTAMP, unit} 161 } 162 163 // Time32TypeUnit returns a TypeMatcher that will match only 164 // a Time32 datatype with the specified TimeUnit. 165 func Time32TypeUnit(unit arrow.TimeUnit) TypeMatcher { 166 return &timeUnitMatcher{arrow.TIME32, unit} 167 } 168 169 // Time64TypeUnit returns a TypeMatcher that will match only 170 // a Time64 datatype with the specified TimeUnit. 171 func Time64TypeUnit(unit arrow.TimeUnit) TypeMatcher { 172 return &timeUnitMatcher{arrow.TIME64, unit} 173 } 174 175 // DurationTypeUnit returns a TypeMatcher that will match only 176 // a Duration datatype with the specified TimeUnit. 177 func DurationTypeUnit(unit arrow.TimeUnit) TypeMatcher { 178 return &timeUnitMatcher{arrow.DURATION, unit} 179 } 180 181 type integerMatcher struct{} 182 183 func (integerMatcher) String() string { return "integer" } 184 func (integerMatcher) Matches(typ arrow.DataType) bool { return arrow.IsInteger(typ.ID()) } 185 func (integerMatcher) Equals(other TypeMatcher) bool { 186 _, ok := other.(integerMatcher) 187 return ok 188 } 189 190 type binaryLikeMatcher struct{} 191 192 func (binaryLikeMatcher) String() string { return "binary-like" } 193 func (binaryLikeMatcher) Matches(typ arrow.DataType) bool { return arrow.IsBinaryLike(typ.ID()) } 194 func (binaryLikeMatcher) Equals(other TypeMatcher) bool { 195 _, ok := other.(binaryLikeMatcher) 196 return ok 197 } 198 199 type largeBinaryLikeMatcher struct{} 200 201 func (largeBinaryLikeMatcher) String() string { return "large-binary-like" } 202 func (largeBinaryLikeMatcher) Matches(typ arrow.DataType) bool { 203 return arrow.IsLargeBinaryLike(typ.ID()) 204 } 205 func (largeBinaryLikeMatcher) Equals(other TypeMatcher) bool { 206 _, ok := other.(largeBinaryLikeMatcher) 207 return ok 208 } 209 210 type fsbLikeMatcher struct{} 211 212 func (fsbLikeMatcher) String() string { return "fixed-size-binary-like" } 213 func (fsbLikeMatcher) Matches(typ arrow.DataType) bool { return arrow.IsFixedSizeBinary(typ.ID()) } 214 func (fsbLikeMatcher) Equals(other TypeMatcher) bool { 215 _, ok := other.(fsbLikeMatcher) 216 return ok 217 } 218 219 // Integer returns a TypeMatcher which will match any integral type like int8 or uint16 220 func Integer() TypeMatcher { return integerMatcher{} } 221 222 // BinaryLike returns a TypeMatcher that will match Binary or String 223 func BinaryLike() TypeMatcher { return binaryLikeMatcher{} } 224 225 // LargeBinaryLike returns a TypeMatcher which will match LargeBinary or LargeString 226 func LargeBinaryLike() TypeMatcher { return largeBinaryLikeMatcher{} } 227 228 // FixedSizeBinaryLike returns a TypeMatcher that will match FixedSizeBinary 229 // or Decimal128/256 230 func FixedSizeBinaryLike() TypeMatcher { return fsbLikeMatcher{} } 231 232 type primitiveMatcher struct{} 233 234 func (primitiveMatcher) String() string { return "primitive" } 235 func (primitiveMatcher) Matches(typ arrow.DataType) bool { return arrow.IsPrimitive(typ.ID()) } 236 func (primitiveMatcher) Equals(other TypeMatcher) bool { 237 _, ok := other.(primitiveMatcher) 238 return ok 239 } 240 241 // Primitive returns a TypeMatcher that will match any type that arrow.IsPrimitive 242 // returns true for. 243 func Primitive() TypeMatcher { return primitiveMatcher{} } 244 245 type reeMatcher struct { 246 runEndsMatcher TypeMatcher 247 encodedMatcher TypeMatcher 248 } 249 250 func (r reeMatcher) Matches(typ arrow.DataType) bool { 251 if typ.ID() != arrow.RUN_END_ENCODED { 252 return false 253 } 254 255 dt := typ.(*arrow.RunEndEncodedType) 256 return r.runEndsMatcher.Matches(dt.RunEnds()) && r.encodedMatcher.Matches(dt.Encoded()) 257 } 258 259 func (r reeMatcher) Equals(other TypeMatcher) bool { 260 o, ok := other.(reeMatcher) 261 if !ok { 262 return false 263 } 264 return r.runEndsMatcher.Equals(o.runEndsMatcher) && r.encodedMatcher.Equals(o.encodedMatcher) 265 } 266 267 func (r reeMatcher) String() string { 268 return "run_end_encoded(run_ends=" + r.runEndsMatcher.String() + ", values=" + r.encodedMatcher.String() + ")" 269 } 270 271 // RunEndEncoded returns a matcher which matches a RunEndEncoded 272 // type whose encoded type is matched by the passed in matcher. 273 func RunEndEncoded(runEndsMatcher, encodedMatcher TypeMatcher) TypeMatcher { 274 return reeMatcher{ 275 runEndsMatcher: runEndsMatcher, 276 encodedMatcher: encodedMatcher} 277 } 278 279 // InputKind is an enum representing the type of Input matching 280 // that will be done. Either accepting any type, an exact specific type 281 // or using a TypeMatcher. 282 type InputKind int8 283 284 const ( 285 InputAny InputKind = iota 286 InputExact 287 InputUseMatcher 288 ) 289 290 // InputType is used for type checking arguments passed to a kernel 291 // and stored within a KernelSignature. The type-checking rule can 292 // be supplied either with an exact DataType instance or a custom 293 // TypeMatcher. 294 type InputType struct { 295 Kind InputKind 296 Type arrow.DataType 297 Matcher TypeMatcher 298 } 299 300 func NewExactInput(dt arrow.DataType) InputType { return InputType{Kind: InputExact, Type: dt} } 301 func NewMatchedInput(match TypeMatcher) InputType { 302 return InputType{Kind: InputUseMatcher, Matcher: match} 303 } 304 func NewIDInput(id arrow.Type) InputType { return NewMatchedInput(SameTypeID(id)) } 305 306 func (it InputType) MatchID() arrow.Type { 307 switch it.Kind { 308 case InputExact: 309 return it.Type.ID() 310 case InputUseMatcher: 311 if idMatch, ok := it.Matcher.(*sameTypeIDMatcher); ok { 312 return idMatch.accepted 313 } 314 } 315 debug.Assert(false, "MatchID called on non-id matching InputType") 316 return -1 317 } 318 319 func (it InputType) String() string { 320 switch it.Kind { 321 case InputAny: 322 return "any" 323 case InputUseMatcher: 324 return it.Matcher.String() 325 case InputExact: 326 return it.Type.String() 327 } 328 return "" 329 } 330 331 func (it *InputType) Equals(other *InputType) bool { 332 if it == other { 333 return true 334 } 335 336 if it.Kind != other.Kind { 337 return false 338 } 339 340 switch it.Kind { 341 case InputAny: 342 return true 343 case InputExact: 344 return arrow.TypeEqual(it.Type, other.Type) 345 case InputUseMatcher: 346 return it.Matcher.Equals(other.Matcher) 347 default: 348 return false 349 } 350 } 351 352 func (it InputType) Hash() uint64 { 353 var h maphash.Hash 354 355 h.SetSeed(hashSeed) 356 result := HashCombine(h.Sum64(), uint64(it.Kind)) 357 switch it.Kind { 358 case InputExact: 359 result = HashCombine(result, arrow.HashType(hashSeed, it.Type)) 360 } 361 return result 362 } 363 364 func (it InputType) Matches(dt arrow.DataType) bool { 365 switch it.Kind { 366 case InputExact: 367 return arrow.TypeEqual(it.Type, dt) 368 case InputUseMatcher: 369 return it.Matcher.Matches(dt) 370 case InputAny: 371 return true 372 default: 373 debug.Assert(false, "invalid InputKind") 374 return true 375 } 376 } 377 378 // ResolveKind defines the way that a particular OutputType resolves 379 // its type. Either it has a fixed type to resolve to or it contains 380 // a Resolver which will compute the resolved type based on 381 // the input types. 382 type ResolveKind int8 383 384 const ( 385 ResolveFixed ResolveKind = iota 386 ResolveComputed 387 ) 388 389 // TypeResolver is simply a function that takes a KernelCtx and a list of input types 390 // and returns the resolved type or an error. 391 type TypeResolver = func(*KernelCtx, []arrow.DataType) (arrow.DataType, error) 392 393 type OutputType struct { 394 Kind ResolveKind 395 Type arrow.DataType 396 Resolver TypeResolver 397 } 398 399 func NewOutputType(dt arrow.DataType) OutputType { 400 return OutputType{Kind: ResolveFixed, Type: dt} 401 } 402 403 func NewComputedOutputType(resolver TypeResolver) OutputType { 404 return OutputType{Kind: ResolveComputed, Resolver: resolver} 405 } 406 407 func (o OutputType) String() string { 408 if o.Kind == ResolveFixed { 409 return o.Type.String() 410 } 411 return "computed" 412 } 413 414 func (o OutputType) Resolve(ctx *KernelCtx, types []arrow.DataType) (arrow.DataType, error) { 415 switch o.Kind { 416 case ResolveFixed: 417 return o.Type, nil 418 } 419 420 return o.Resolver(ctx, types) 421 } 422 423 // NullHandling is an enum representing how a particular Kernel 424 // wants the executor to handle nulls. 425 type NullHandling int8 426 427 const ( 428 // Compute the output validity bitmap by intersection the validity 429 // bitmaps of the arguments using bitwise-and operations. This means 430 // that values in the output are valid/non-null only if the corresponding 431 // values in all input arguments were valid/non-null. Kernels generally 432 // do not have to touch the bitmap afterwards, but a kernel's exec function 433 // is permitted to alter the bitmap after the null intersection is computed 434 // if necessary. 435 NullIntersection NullHandling = iota 436 // Kernel expects a pre-allocated buffer to write the result bitmap 437 // into. 438 NullComputedPrealloc 439 // Kernel will allocate and set the validity bitmap of the output 440 NullComputedNoPrealloc 441 // kernel output is never null and a validity bitmap doesn't need to 442 // be allocated 443 NullNoOutput 444 ) 445 446 // MemAlloc is the preference for preallocating memory of fixed-width 447 // type outputs during kernel execution. 448 type MemAlloc int8 449 450 const ( 451 // For data types that support pre-allocation (fixed-width), the 452 // kernel expects to be provided a pre-allocated buffer to write into. 453 // Non-fixed-width types must always allocate their own buffers. 454 // The allocation is made for the same length as the execution batch, 455 // so vector kernels yielding differently sized outputs should not 456 // use this. 457 // 458 // It is valid for the data to not be preallocated but the validity 459 // bitmap is (or is computed using intersection). 460 // 461 // For variable-size output types like Binary or String, or for nested 462 // types, this option has no effect. 463 MemPrealloc MemAlloc = iota 464 // The kernel is responsible for allocating its own data buffer 465 // for fixed-width output types. 466 MemNoPrealloc 467 ) 468 469 type KernelState any 470 471 // KernelInitArgs are the arguments required to initialize an Kernel's 472 // state using the input types and any options. 473 type KernelInitArgs struct { 474 Kernel Kernel 475 Inputs []arrow.DataType 476 // Options are opaque and specific to the Kernel being initialized, 477 // may be nil if the kernel doesn't require options. 478 Options any 479 } 480 481 // KernelInitFn is any function that receives a KernelCtx and initialization 482 // arguments and returns the initialized state or an error. 483 type KernelInitFn = func(*KernelCtx, KernelInitArgs) (KernelState, error) 484 485 // KernelSignature holds the input and output types for a kernel. 486 // 487 // Variable argument functions with a minimum of N arguments should pass 488 // up to N input types to be used to validate for invocation. The first 489 // N-1 types will be matched against the first N-1 arguments and the last 490 // type will be matched against the remaining arguments. 491 type KernelSignature struct { 492 InputTypes []InputType 493 OutType OutputType 494 IsVarArgs bool 495 496 // store the hashcode after it is computed so we don't 497 // need to recompute it 498 hashCode uint64 499 } 500 501 func (k KernelSignature) String() string { 502 var b strings.Builder 503 if k.IsVarArgs { 504 b.WriteString("varargs[") 505 } else { 506 b.WriteByte('(') 507 } 508 509 for i, t := range k.InputTypes { 510 if i != 0 { 511 b.WriteString(", ") 512 } 513 b.WriteString(t.String()) 514 } 515 if k.IsVarArgs { 516 b.WriteString("*]") 517 } else { 518 b.WriteByte(')') 519 } 520 521 b.WriteString(" -> ") 522 b.WriteString(k.OutType.String()) 523 return b.String() 524 } 525 526 func (k KernelSignature) Equals(other KernelSignature) bool { 527 if k.IsVarArgs != other.IsVarArgs { 528 return false 529 } 530 531 return slices.EqualFunc(k.InputTypes, other.InputTypes, func(e1, e2 InputType) bool { 532 return e1.Equals(&e2) 533 }) 534 } 535 536 func (k *KernelSignature) Hash() uint64 { 537 if k.hashCode != 0 { 538 return k.hashCode 539 } 540 541 var h maphash.Hash 542 h.SetSeed(hashSeed) 543 result := h.Sum64() 544 for _, typ := range k.InputTypes { 545 result = HashCombine(result, typ.Hash()) 546 } 547 k.hashCode = result 548 return result 549 } 550 551 func (k KernelSignature) MatchesInputs(types []arrow.DataType) bool { 552 switch k.IsVarArgs { 553 case true: 554 // check that it has enough to match at least the non-vararg types 555 if len(types) < (len(k.InputTypes) - 1) { 556 return false 557 } 558 559 for i, t := range types { 560 if !k.InputTypes[Min(i, len(k.InputTypes)-1)].Matches(t) { 561 return false 562 } 563 } 564 case false: 565 if len(types) != len(k.InputTypes) { 566 return false 567 } 568 for i, t := range types { 569 if !k.InputTypes[i].Matches(t) { 570 return false 571 } 572 } 573 } 574 return true 575 } 576 577 // ArrayKernelExec is an alias definition for a kernel's execution function. 578 // 579 // This is used for both stateless and stateful kernels. If a kernel 580 // depends on some execution state, it can be accessed from the KernelCtx 581 // object, which also contains the context.Context object which can be 582 // used for shortcircuiting by checking context.Done / context.Err. 583 // This allows kernels to control handling timeouts or cancellation of 584 // computation. 585 type ArrayKernelExec = func(*KernelCtx, *ExecSpan, *ExecResult) error 586 587 type kernel struct { 588 Init KernelInitFn 589 Signature *KernelSignature 590 Data KernelState 591 Parallelizable bool 592 } 593 594 func (k kernel) GetInitFn() KernelInitFn { return k.Init } 595 func (k kernel) GetSig() *KernelSignature { return k.Signature } 596 597 // A ScalarKernel is the kernel implementation for a Scalar Function. 598 // In addition to the members found in the base Kernel, it contains 599 // the null handling and memory pre-allocation preferences. 600 type ScalarKernel struct { 601 kernel 602 603 ExecFn ArrayKernelExec 604 CanWriteIntoSlices bool 605 NullHandling NullHandling 606 MemAlloc MemAlloc 607 } 608 609 // NewScalarKernel constructs a new kernel for scalar execution, constructing 610 // a KernelSignature with the provided input types and output type, and using 611 // the passed in execution implementation and initialization function. 612 func NewScalarKernel(in []InputType, out OutputType, exec ArrayKernelExec, init KernelInitFn) ScalarKernel { 613 return NewScalarKernelWithSig(&KernelSignature{ 614 InputTypes: in, 615 OutType: out, 616 }, exec, init) 617 } 618 619 // NewScalarKernelWithSig is a convenience when you already have a signature 620 // to use for constructing a kernel. It's equivalent to passing the components 621 // of the signature (input and output types) to NewScalarKernel. 622 func NewScalarKernelWithSig(sig *KernelSignature, exec ArrayKernelExec, init KernelInitFn) ScalarKernel { 623 return ScalarKernel{ 624 kernel: kernel{Signature: sig, Init: init, Parallelizable: true}, 625 ExecFn: exec, 626 CanWriteIntoSlices: true, 627 NullHandling: NullIntersection, 628 MemAlloc: MemPrealloc, 629 } 630 } 631 632 func (s *ScalarKernel) Exec(ctx *KernelCtx, sp *ExecSpan, out *ExecResult) error { 633 return s.ExecFn(ctx, sp, out) 634 } 635 636 func (s ScalarKernel) GetNullHandling() NullHandling { return s.NullHandling } 637 func (s ScalarKernel) GetMemAlloc() MemAlloc { return s.MemAlloc } 638 func (s ScalarKernel) CanFillSlices() bool { return s.CanWriteIntoSlices } 639 640 // ChunkedExec is the signature for executing a stateful vector kernel 641 // against a ChunkedArray input. It is optional 642 type ChunkedExec func(*KernelCtx, []*arrow.Chunked, *ExecResult) ([]*ExecResult, error) 643 644 // FinalizeFunc is an optional finalizer function for any postprocessing 645 // that may need to be done on data before returning it 646 type FinalizeFunc func(*KernelCtx, []*ArraySpan) ([]*ArraySpan, error) 647 648 // VectorKernel is a structure for implementations of vector functions. 649 // It can optionally contain a finalizer function, the null handling 650 // and memory pre-allocation preferences (different defaults from 651 // scalar kernels when using NewVectorKernel), and other execution related 652 // options. 653 type VectorKernel struct { 654 kernel 655 656 ExecFn ArrayKernelExec 657 ExecChunked ChunkedExec 658 Finalize FinalizeFunc 659 NullHandling NullHandling 660 MemAlloc MemAlloc 661 CanWriteIntoSlices bool 662 CanExecuteChunkWise bool 663 OutputChunked bool 664 } 665 666 // NewVectorKernel constructs a new kernel for execution of vector functions, 667 // which take into account more than just the individual scalar values 668 // of its input. Output of a vector kernel may be a different length 669 // than its inputs. 670 func NewVectorKernel(inTypes []InputType, outType OutputType, exec ArrayKernelExec, init KernelInitFn) VectorKernel { 671 return NewVectorKernelWithSig(&KernelSignature{ 672 InputTypes: inTypes, OutType: outType}, exec, init) 673 } 674 675 // NewVectorKernelWithSig is a convenience function for creating a kernel 676 // when you already have a signature constructed. 677 func NewVectorKernelWithSig(sig *KernelSignature, exec ArrayKernelExec, init KernelInitFn) VectorKernel { 678 return VectorKernel{ 679 kernel: kernel{Signature: sig, Init: init, Parallelizable: true}, 680 ExecFn: exec, 681 CanWriteIntoSlices: true, 682 CanExecuteChunkWise: true, 683 OutputChunked: true, 684 NullHandling: NullComputedNoPrealloc, 685 MemAlloc: MemNoPrealloc, 686 } 687 } 688 689 func (s *VectorKernel) Exec(ctx *KernelCtx, sp *ExecSpan, out *ExecResult) error { 690 return s.ExecFn(ctx, sp, out) 691 } 692 693 func (s VectorKernel) GetNullHandling() NullHandling { return s.NullHandling } 694 func (s VectorKernel) GetMemAlloc() MemAlloc { return s.MemAlloc } 695 func (s VectorKernel) CanFillSlices() bool { return s.CanWriteIntoSlices }