Skip to content

Commit 5569450

Browse files
authored
Avoid clone in vec_cast() with shaped objects when nothing changes (#2109)
* Only clone with shaped casting if dims/dim names change So the self cast remains free * Add golden tests * NEWS bullet
1 parent 5b75ec0 commit 5569450

File tree

6 files changed

+82
-26
lines changed

6 files changed

+82
-26
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# vctrs (development version)
22

3+
* `vec_cast()` with arrays no longer clones when no casting is required (#2006).
4+
35
* `vec_rank()` now throws an improved error on non-vector types, like `NULL` (#1967).
46

57
* `vec_ptype_common()` has gained a `.finalise` argument that defaults to `TRUE`. Setting this to `FALSE` lets you opt out of prototype finalisation, which allows `vec_ptype_common()` to act like `vec_ptype()` and `vec_ptype2()`, which don't finalise. This can be useful in some advanced common type determination cases (#2100).

src/cast.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@ r_obj* ffi_cast(r_obj* x,
2020
r_obj* vec_cast_opts(const struct cast_opts* opts) {
2121
r_obj* x = opts->x;
2222
r_obj* to = opts->to;
23-
struct vctrs_arg* x_arg = opts->p_x_arg;
24-
struct vctrs_arg* to_arg = opts->p_to_arg;
23+
struct vctrs_arg* p_x_arg = opts->p_x_arg;
24+
struct vctrs_arg* p_to_arg = opts->p_to_arg;
25+
struct r_lazy call = opts->call;
2526

2627
if (x == r_null) {
2728
// Allow both `vec_cast(NULL, <vector>)` and `vec_cast(NULL, NULL)`
28-
obj_check_vector(to, VCTRS_ALLOW_NULL_yes, to_arg, opts->call);
29+
obj_check_vector(to, VCTRS_ALLOW_NULL_yes, p_to_arg, call);
2930
return x;
3031
}
3132
if (to == r_null) {
3233
// Allow `vec_cast(<vector>, NULL)`
33-
obj_check_vector(x, VCTRS_ALLOW_NULL_no, x_arg, opts->call);
34+
obj_check_vector(x, VCTRS_ALLOW_NULL_no, p_x_arg, call);
3435
return x;
3536
}
3637

@@ -42,10 +43,10 @@ r_obj* vec_cast_opts(const struct cast_opts* opts) {
4243
}
4344

4445
if (x_type == VCTRS_TYPE_scalar) {
45-
stop_scalar_type(x, x_arg, opts->call);
46+
stop_scalar_type(x, p_x_arg, call);
4647
}
4748
if (to_type == VCTRS_TYPE_scalar) {
48-
stop_scalar_type(to, to_arg, opts->call);
49+
stop_scalar_type(to, p_to_arg, call);
4950
}
5051

5152
r_obj* out = r_null;
@@ -64,7 +65,26 @@ r_obj* vec_cast_opts(const struct cast_opts* opts) {
6465
}
6566

6667
if (has_dim(x) || has_dim(to)) {
67-
out = vec_shape_broadcast(out, opts);
68+
r_obj* x_dim = r_dim(x);
69+
r_obj* x_dim_names = r_dim_names(x);
70+
71+
// Ensure `out` has the shape of `x`.
72+
// Native casting doesn't propagate shape.
73+
if (
74+
!equal_object(r_dim(out), x_dim) ||
75+
!equal_object(r_dim_names(out), x_dim_names)
76+
) {
77+
out = KEEP(r_clone_referenced(out));
78+
r_attrib_poke_dim(out, x_dim);
79+
r_attrib_poke_dim_names(out, x_dim_names);
80+
FREE(1);
81+
}
82+
KEEP(out);
83+
84+
// Broadcast `out` to the shape of `to`
85+
out = vec_shape_broadcast(out, to, p_x_arg, p_to_arg, call);
86+
87+
FREE(1);
6888
}
6989

7090
FREE(1);

src/shape.c

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -186,30 +186,31 @@ int dim2(
186186

187187
// -----------------------------------------------------------------------------
188188

189-
r_obj* vec_shape_broadcast(r_obj* out, const struct cast_opts* p_opts) {
190-
r_obj* r_x_arg = KEEP(vctrs_arg(p_opts->p_x_arg));
191-
r_obj* r_to_arg = KEEP(vctrs_arg(p_opts->p_to_arg));
192-
r_obj* call = KEEP(r_lazy_eval(p_opts->call));
193-
194-
out = KEEP(r_clone_referenced(out));
195-
196-
r_attrib_poke_dim(out, r_dim(p_opts->x));
197-
r_attrib_poke_dim_names(out, r_dim_names(p_opts->x));
189+
r_obj* vec_shape_broadcast(
190+
r_obj* x,
191+
r_obj* to,
192+
struct vctrs_arg* p_x_arg,
193+
struct vctrs_arg* p_to_arg,
194+
struct r_lazy call
195+
) {
196+
r_obj* ffi_x_arg = KEEP(vctrs_arg(p_x_arg));
197+
r_obj* ffi_to_arg = KEEP(vctrs_arg(p_to_arg));
198+
r_obj* ffi_call = KEEP(r_lazy_eval(call));
198199

199-
out = vctrs_eval_mask5(
200+
r_obj* out = vctrs_eval_mask5(
200201
r_sym("shape_broadcast"),
201202
r_syms.x,
202-
out,
203+
x,
203204
r_sym("to"),
204-
p_opts->to,
205+
to,
205206
syms.x_arg,
206-
r_x_arg,
207+
ffi_x_arg,
207208
syms.to_arg,
208-
r_to_arg,
209+
ffi_to_arg,
209210
r_syms.call,
210-
call
211+
ffi_call
211212
);
212213

213-
FREE(4);
214+
FREE(3);
214215
return out;
215216
}

src/shape.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
#define VCTRS_SHAPE_H
33

44
#include "vctrs-core.h"
5-
#include "cast.h"
65

76
// Computes the common shape of `x` and `y` and attaches it as the
87
// dimensions of `ptype`. If `x` and `y` are both atomic with `NULL` dimensions,
@@ -15,7 +14,12 @@ r_obj* vec_shaped_ptype(
1514
struct vctrs_arg* p_y_arg
1615
);
1716

18-
r_obj* vec_shape_broadcast(r_obj* out, const struct cast_opts* p_opts);
19-
17+
r_obj* vec_shape_broadcast(
18+
r_obj* x,
19+
r_obj* to,
20+
struct vctrs_arg* p_x_arg,
21+
struct vctrs_arg* p_to_arg,
22+
struct r_lazy call
23+
);
2024

2125
#endif

tests/testthat/_snaps/cast.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,17 @@
9191
Error:
9292
! Can't convert `TRUE` <logical> to <vctrs_unspecified>.
9393

94+
# casting performs expected allocations
95+
96+
Code
97+
x <- matrix(rep(1L, 100), ncol = 2)
98+
with_memory_prof(vec_cast(x, x))
99+
Output
100+
[1] 0B
101+
Code
102+
x <- matrix(rep(1L, 100), ncol = 2)
103+
y <- matrix(rep(1, 100), ncol = 2)
104+
with_memory_prof(vec_cast(x, y))
105+
Output
106+
[1] 848B
107+

tests/testthat/test-cast.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,18 @@ test_that("can cast to unspecified `NA` with `vec_cast()` and `vec_cast_common()
357357
# so this technically works (but again, it is an edge case)
358358
expect_identical(vec_cast_common(TRUE, .to = unspecified(1)), list(TRUE))
359359
})
360+
361+
# Golden tests -------------------------------------------------------
362+
363+
test_that("casting performs expected allocations", {
364+
expect_snapshot({
365+
# No allocations when shape doesn't change (#2006)
366+
x <- matrix(rep(1L, 1e2), ncol = 2)
367+
with_memory_prof(vec_cast(x, x))
368+
369+
# One allocation when type changes
370+
x <- matrix(rep(1L, 1e2), ncol = 2)
371+
y <- matrix(rep(1, 1e2), ncol = 2)
372+
with_memory_prof(vec_cast(x, y))
373+
})
374+
})

0 commit comments

Comments
 (0)