From 7c73bf0d4f760066feb7256896375727656e398d Mon Sep 17 00:00:00 2001 From: Maximilian Muecke Date: Sat, 10 Oct 2026 12:05:24 +0200 Subject: [PATCH 01/11] fmelt: create the string var once per measure group instead of once per row --- src/fmelt.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fmelt.c b/src/fmelt.c index 7db6b09916..ebf35763c8 100644 --- a/src/fmelt.c +++ b/src/fmelt.c @@ -611,7 +611,8 @@ SEXP getvarcols(SEXP DT, SEXP dtnames, Rboolean varfactor, Rboolean verbose, str const int thislen = data->narm ? length(VECTOR_ELT(data->not_NA_indices, j)) : data->nrow; char buff[20]; snprintf(buff, sizeof(buff), "%d", level++); // # notranslate - for (int k=0; k Date: Sat, 10 Oct 2026 12:10:41 +0200 Subject: [PATCH 02/11] fmelt: hoist REAL()/INTEGER() out of the row loop --- src/fmelt.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/fmelt.c b/src/fmelt.c index ebf35763c8..e519815650 100644 --- a/src/fmelt.c +++ b/src/fmelt.c @@ -667,20 +667,24 @@ SEXP getvarcols(SEXP DT, SEXP dtnames, Rboolean varfactor, Rboolean verbose, str for (int k=0; kvariable_table, R_NamesSymbol), out_col_i))); } From 45c63fa0410c441cbc3764d3fa19b47344d0e418 Mon Sep 17 00:00:00 2001 From: Maximilian Muecke Date: Sun, 11 Oct 2026 16:54:08 +0200 Subject: [PATCH 03/11] fmelt: hoist STRING_ELT() out of the row loop for char cols --- src/fmelt.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fmelt.c b/src/fmelt.c index e519815650..fc7235febf 100644 --- a/src/fmelt.c +++ b/src/fmelt.c @@ -663,10 +663,11 @@ SEXP getvarcols(SEXP DT, SEXP dtnames, Rboolean varfactor, Rboolean verbose, str for (int j=0, ansloc=0; jlmax; ++j) { const int thislen = data->narm ? length(VECTOR_ELT(data->not_NA_indices, j)) : data->nrow; switch (TYPEOF(target)) { - case STRSXP : + case STRSXP : { + const SEXP val = STRING_ELT(out_col, j); for (int k=0; k Date: Tue, 13 Oct 2026 21:04:35 +0200 Subject: [PATCH 04/11] atime: add tests for melt improvements --- .ci/atime/tests.R | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index 2d342ded67..6da2c59b5a 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -459,5 +459,26 @@ test.list <- atime::atime_test_list( seconds.limit = 1, expr = data.table:::`[.data.table`(dt, , base::max(V1, na.rm = TRUE), by = id)), + # https://github.com/Rdatatable/data.table/pull/7917 creates the variable string once per measure group instead of once per row. + "melt(variable.factor=FALSE) improved in #7917" = atime::atime_test( + setup = { + DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) + }, + Slow = "df59dd637403e65d7fa90d48bb0dfec2764d6744", # Parent of the first commit (https://github.com/Rdatatable/data.table/commit/7c73bf0d4f760066feb7256896375727656e398d) in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that improves efficiency + Fast = "7c73bf0d4f760066feb7256896375727656e398d", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that creates the variable string once per measure group + expr = data.table:::melt(DT, measure.vars = list(c("a_1", "a_2"), c("b_1", "b_2")), variable.factor = FALSE)), + + # https://github.com/Rdatatable/data.table/pull/7917 hoists REAL()/INTEGER() out of the row loop when filling variable_table columns. + "melt(variable_table) improved in #7917" = atime::atime_test( + setup = { + DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) + # Same as measure(value.name, num=as.integer), which errors under atime since cedta() does not recognize the renamed package. + measure.vars = list(a = c("a_1", "a_2"), b = c("b_1", "b_2")) + attr(measure.vars, "variable_table") = data.table(num = 1:2) + }, + Slow = "df59dd637403e65d7fa90d48bb0dfec2764d6744", # Parent of the first commit (https://github.com/Rdatatable/data.table/commit/7c73bf0d4f760066feb7256896375727656e398d) in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that improves efficiency + Fast = "96b9ab84a87d2c115e006e52027045f82549d377", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that hoists REAL()/INTEGER() out of the row loop + expr = data.table:::melt(DT, measure.vars = measure.vars)), + tests=extra.test.list) # nolint end: undesirable_operator_linter. From bf885d71f60a0bf246ad8758f4636cc46a084d9f Mon Sep 17 00:00:00 2001 From: Maximilian Muecke Date: Wed, 14 Oct 2026 22:43:55 +0200 Subject: [PATCH 05/11] atime: use measure() in the variable_table melt test --- .ci/atime/tests.R | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index 6da2c59b5a..98fe522f57 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -472,13 +472,10 @@ test.list <- atime::atime_test_list( "melt(variable_table) improved in #7917" = atime::atime_test( setup = { DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) - # Same as measure(value.name, num=as.integer), which errors under atime since cedta() does not recognize the renamed package. - measure.vars = list(a = c("a_1", "a_2"), b = c("b_1", "b_2")) - attr(measure.vars, "variable_table") = data.table(num = 1:2) }, Slow = "df59dd637403e65d7fa90d48bb0dfec2764d6744", # Parent of the first commit (https://github.com/Rdatatable/data.table/commit/7c73bf0d4f760066feb7256896375727656e398d) in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that improves efficiency Fast = "96b9ab84a87d2c115e006e52027045f82549d377", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that hoists REAL()/INTEGER() out of the row loop - expr = data.table:::melt(DT, measure.vars = measure.vars)), + expr = data.table:::melt(DT, measure.vars = measure(letter, number = as.integer))), tests=extra.test.list) # nolint end: undesirable_operator_linter. From cd9c02539642ac40153ad73d2fba08316bcfbca7 Mon Sep 17 00:00:00 2001 From: Maximilian Muecke Date: Wed, 14 Oct 2026 22:55:32 +0200 Subject: [PATCH 06/11] atime: update variable_table melt Fast commit --- .ci/atime/tests.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index 98fe522f57..7f7325e886 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -468,13 +468,13 @@ test.list <- atime::atime_test_list( Fast = "7c73bf0d4f760066feb7256896375727656e398d", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that creates the variable string once per measure group expr = data.table:::melt(DT, measure.vars = list(c("a_1", "a_2"), c("b_1", "b_2")), variable.factor = FALSE)), - # https://github.com/Rdatatable/data.table/pull/7917 hoists REAL()/INTEGER() out of the row loop when filling variable_table columns. + # https://github.com/Rdatatable/data.table/pull/7917 hoists REAL()/INTEGER()/STRING_ELT() out of the row loop when filling variable_table columns. "melt(variable_table) improved in #7917" = atime::atime_test( setup = { DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) }, Slow = "df59dd637403e65d7fa90d48bb0dfec2764d6744", # Parent of the first commit (https://github.com/Rdatatable/data.table/commit/7c73bf0d4f760066feb7256896375727656e398d) in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that improves efficiency - Fast = "96b9ab84a87d2c115e006e52027045f82549d377", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that hoists REAL()/INTEGER() out of the row loop + Fast = "45c63fa0410c441cbc3764d3fa19b47344d0e418", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that hoists REAL()/INTEGER()/STRING_ELT() out of the row loop expr = data.table:::melt(DT, measure.vars = measure(letter, number = as.integer))), tests=extra.test.list) From 9d0fecbf61b14bb2080d453e45d09af16470add2 Mon Sep 17 00:00:00 2001 From: Maximilian Muecke Date: Thu, 15 Oct 2026 10:15:12 +0200 Subject: [PATCH 07/11] atime: clarify melt test name and comment --- .ci/atime/tests.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index 7f7325e886..72d779be13 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -460,7 +460,7 @@ test.list <- atime::atime_test_list( expr = data.table:::`[.data.table`(dt, , base::max(V1, na.rm = TRUE), by = id)), # https://github.com/Rdatatable/data.table/pull/7917 creates the variable string once per measure group instead of once per row. - "melt(variable.factor=FALSE) improved in #7917" = atime::atime_test( + "melt(list measure.vars, variable.factor=FALSE) improved in #7917" = atime::atime_test( setup = { DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) }, @@ -468,7 +468,7 @@ test.list <- atime::atime_test_list( Fast = "7c73bf0d4f760066feb7256896375727656e398d", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that creates the variable string once per measure group expr = data.table:::melt(DT, measure.vars = list(c("a_1", "a_2"), c("b_1", "b_2")), variable.factor = FALSE)), - # https://github.com/Rdatatable/data.table/pull/7917 hoists REAL()/INTEGER()/STRING_ELT() out of the row loop when filling variable_table columns. + # https://github.com/Rdatatable/data.table/pull/7917 hoists INTEGER()/STRING_ELT() out of the row loop when filling variable_table columns. "melt(variable_table) improved in #7917" = atime::atime_test( setup = { DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) From ef762fe0fb96018e14bb39fe953f779a7347a37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20M=C3=BCcke?= Date: Thu, 15 Oct 2026 17:09:56 +0200 Subject: [PATCH 08/11] Update .ci/atime/tests.R Co-authored-by: Toby Dylan Hocking --- .ci/atime/tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index 72d779be13..c9f2c825be 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -469,7 +469,7 @@ test.list <- atime::atime_test_list( expr = data.table:::melt(DT, measure.vars = list(c("a_1", "a_2"), c("b_1", "b_2")), variable.factor = FALSE)), # https://github.com/Rdatatable/data.table/pull/7917 hoists INTEGER()/STRING_ELT() out of the row loop when filling variable_table columns. - "melt(variable_table) improved in #7917" = atime::atime_test( + "melt(measure()) improved in #7917" = atime::atime_test( setup = { DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) }, From 4d61643f7e1576d3c2165f7119eb47f6389d46d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20M=C3=BCcke?= Date: Thu, 15 Oct 2026 17:24:51 +0200 Subject: [PATCH 09/11] Update .ci/atime/tests.R Co-authored-by: Toby Dylan Hocking --- .ci/atime/tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index c9f2c825be..ece3c1717a 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -465,7 +465,7 @@ test.list <- atime::atime_test_list( DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) }, Slow = "df59dd637403e65d7fa90d48bb0dfec2764d6744", # Parent of the first commit (https://github.com/Rdatatable/data.table/commit/7c73bf0d4f760066feb7256896375727656e398d) in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that improves efficiency - Fast = "7c73bf0d4f760066feb7256896375727656e398d", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that creates the variable string once per measure group + Fast = "7c73bf0d4f760066feb7256896375727656e398d", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that creates the variable string once per measure group TODO after merging PR7917, change to its merge commit expr = data.table:::melt(DT, measure.vars = list(c("a_1", "a_2"), c("b_1", "b_2")), variable.factor = FALSE)), # https://github.com/Rdatatable/data.table/pull/7917 hoists INTEGER()/STRING_ELT() out of the row loop when filling variable_table columns. From 40b5db994e030aad6aaa84ed5aecb717762497a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20M=C3=BCcke?= Date: Thu, 15 Oct 2026 17:25:20 +0200 Subject: [PATCH 10/11] Update .ci/atime/tests.R Co-authored-by: Toby Dylan Hocking --- .ci/atime/tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index ece3c1717a..4b9e6ea92e 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -474,7 +474,7 @@ test.list <- atime::atime_test_list( DT = data.table(id = seq_len(N), a_1 = 1, a_2 = 2, b_1 = 3, b_2 = 4) }, Slow = "df59dd637403e65d7fa90d48bb0dfec2764d6744", # Parent of the first commit (https://github.com/Rdatatable/data.table/commit/7c73bf0d4f760066feb7256896375727656e398d) in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that improves efficiency - Fast = "45c63fa0410c441cbc3764d3fa19b47344d0e418", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that hoists REAL()/INTEGER()/STRING_ELT() out of the row loop + Fast = "45c63fa0410c441cbc3764d3fa19b47344d0e418", # Commit in the PR (https://github.com/Rdatatable/data.table/pull/7917/commits) that hoists REAL()/INTEGER()/STRING_ELT() out of the row loop TODO after merging PR7917, change to its merge commit expr = data.table:::melt(DT, measure.vars = measure(letter, number = as.integer))), tests=extra.test.list) From b7cf39656b2e5a57953d0d0e31a8fc4c0e1120be Mon Sep 17 00:00:00 2001 From: Maximilian Muecke Date: Fri, 16 Oct 2026 21:50:38 +0200 Subject: [PATCH 11/11] Add NEWS item for faster `melt()` --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index ee2f96fd97..cb4a6231fb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -48,6 +48,8 @@ 13. `nafill()` and `setnafill()` gain a `limit` argument to restrict the maximum number of consecutive `NA` values filled, [#7677](https://github.com/Rdatatable/data.table/issues/7677). Thanks to @jaynewton for the suggestion and @venom1204 and @ben-schwen for the PR. +14. `melt()` is faster on large inputs when `measure.vars` is a list with `variable.factor=FALSE` (e.g. 3x) or uses `measure()` (e.g. 2x). Thanks @m-muecke for the PR. + ### BUG FIXES 1. `fread()` with `skip=0` and `(header=TRUE|FALSE)` no longer skips the first row when it has fewer fields than subsequent rows, [#7463](https://github.com/Rdatatable/data.table/issues/7463). Thanks @emayerhofer for the report and @ben-schwen for the fix.