From 45f2ea6281db6e3dac784c0adc83dce6537d3241 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 12 Sep 2026 19:06:45 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EA=B3=A0=EC=9C=A0?= =?UTF-8?q?=EA=B0=92=20=EA=B3=84=EC=82=B0=20=EC=8B=9C=20na.omit=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20logical=20indexing=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stats::na.omit 호출로 인한 불필요한 메서드 디스패치 및 속성 할당을 방지하여 sum(!is.na(unique(x))) 패턴으로 성능을 최적화함. --- .jules/bolt.md | 3 +++ R/aFIPC.R | 4 ++-- R/surveyFA.R | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 7d3c603f..04e6ef5d 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -16,3 +16,6 @@ ## 2025-02-12 - R 언어에서 반복적인 mirt 모델 생성 시 불필요한 데이터프레임 부분집합 추출 최적화 **Learning:** R에서 데이터프레임의 특정 열을 추출하는 작업(`df[cols]`)은 O(N)의 메모리 복사를 수반합니다. `autoFIPC`에서 `mirt` 모델의 파라미터를 설정하거나 호출하는 과정 중에 `newformXDataK[colnames(newFormModel@Data$data)]` 코드가 반복해서 사용되었고, 심지어 `ncol()`을 위해 단순히 개수를 구할 때도 사용되어 불필요한 메모리 할당과 오버헤드를 초래했습니다. **Action:** 조건문이나 반복문 내부에서 불필요하게 데이터프레임 부분집합 연산이 반복되지 않도록 외부에서 한 번만 `linkedFormData <- newformXDataK[colnames(newFormModel@Data$data)]`로 캐싱(caching)한 뒤, `ncol(linkedFormData)`와 `data = linkedFormData` 형태로 재사용하여 메모리 복사와 O(N) 오버헤드를 방지해야 합니다. +## 2024-09-12 - [na.omit() 대신 logical indexing을 활용한 고유값 계산 최적화] +**Learning:** R에서 `length(unique(stats::na.omit(x)))` 대신 `sum(!is.na(unique(x)))`를 사용하면 메서드 디스패치와 추가 메모리 할당(na.action) 오버헤드를 줄여 실행 성능을 높일 수 있다. +**Action:** 고유값 개수를 샐 때 NA를 제외하는 경우 항상 논리 인덱싱의 합계를 사용하는 패턴을 적용해야 한다. diff --git a/R/aFIPC.R b/R/aFIPC.R index 62546519..c8bdc58a 100644 --- a/R/aFIPC.R +++ b/R/aFIPC.R @@ -770,8 +770,8 @@ autoFIPC <- if ( !is.na(newFormItemName) && !is.na(oldFormItemName) && - (length(stats::na.omit(unique(newFormModel@Data$data[, newFormItemName]))) == - length(stats::na.omit(unique(oldFormModel@Data$data[, oldFormItemName])))) + (sum(!is.na(unique(newFormModel@Data$data[, newFormItemName]))) == + sum(!is.na(unique(oldFormModel@Data$data[, oldFormItemName])))) ) { message( 'applying ', diff --git a/R/surveyFA.R b/R/surveyFA.R index f60fffd8..787c1cc9 100644 --- a/R/surveyFA.R +++ b/R/surveyFA.R @@ -83,7 +83,7 @@ surveyFA <- function( response_data <- as.data.frame(data) response_data <- response_data[, vapply(response_data, function(column) { - nunique <- length(unique(stats::na.omit(column))) + nunique <- sum(!is.na(unique(column))) nunique >= 2L }, logical(1L))] From 3131dba259cfec0f080d9e52b980feeb5cdd991d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 16:00:03 +0900 Subject: [PATCH 2/3] test: pin non-missing category count semantics --- .../testthat/test-optimization-equivalence.R | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/testthat/test-optimization-equivalence.R b/tests/testthat/test-optimization-equivalence.R index 02ce2f74..7399a66c 100644 --- a/tests/testthat/test-optimization-equivalence.R +++ b/tests/testthat/test-optimization-equivalence.R @@ -1,6 +1,6 @@ # Formula-integrity regression guards for performance refactors. # -# These tests pin the two formula-bearing expressions that recent "Bolt" +# These tests pin the formula-bearing expressions that recent "Bolt" # performance refactors rewrote, so any future re-optimization that silently # changes their meaning is caught. Values below are hand-computed references, # not a re-encoding of the current implementation. @@ -18,36 +18,52 @@ # Row 1 = old-form anchor names, row 2 = new-form anchor names, restricted # to the columns that survived IPD screening (CommonItemList_NOIPD). -test_that("category-count guard counts distinct non-missing categories (#56)", { +test_that("category-count guard counts distinct non-missing categories (#56, #367)", { vecs <- list( - dichotomous = c(0, 1, 0, 1, 1, 0), + dichotomous = c(0, 1, 0, 1, 1, 0), trichotomous_w_na = c(0, 1, 2, NA, 2, 1, 0), - constant = c(0, 0, 0, 0), + constant = c(0, 0, 0, 0), + all_missing = c(NA_real_, NA_real_), + nan_and_na = c(1, NaN, NA, 1), + factor_w_na = factor(c("a", "b", NA, "a")), four_category_w_na = c(0, 1, 2, 3, 3, NA, 1) ) # Independent hand-computed reference (distinct non-missing categories). expected <- c( - dichotomous = 2L, - trichotomous_w_na = 3L, - constant = 1L, - four_category_w_na = 4L + dichotomous = 2, + trichotomous_w_na = 3, + constant = 1, + all_missing = 0, + nan_and_na = 1, + factor_w_na = 2, + four_category_w_na = 4 ) - new_idiom <- vapply( + previous_idiom <- vapply( vecs, function(x) length(na.omit(unique(x))), - integer(1) + numeric(1) + ) + current_idiom <- vapply( + vecs, + function(x) sum(!is.na(unique(x))), + numeric(1) ) legacy_idiom <- vapply( vecs, function(x) length(levels(as.factor(x))), - integer(1) + numeric(1) ) - expect_equal(new_idiom, expected) - # The refactor must remain equivalent to the pre-#56 expression. - expect_equal(unname(new_idiom), unname(legacy_idiom)) + expect_equal(previous_idiom, expected) + expect_equal(current_idiom, expected) + expect_equal(current_idiom, previous_idiom) + + # The historical factor-level expression is equivalent for the ordinary + # observed-category fixtures where conversion to factor is well-defined. + ordinary <- c("dichotomous", "trichotomous_w_na", "constant", "four_category_w_na") + expect_equal(unname(previous_idiom[ordinary]), unname(legacy_idiom[ordinary])) }) test_that("IPD anchor extraction keeps old/new rows and screened columns (#99)", { From 036f380e20d53fb45ff47c0aa7efec77383970a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 16:00:32 +0900 Subject: [PATCH 3/3] docs: remove unmeasured optimization doctrine --- .jules/bolt.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 04e6ef5d..7d3c603f 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -16,6 +16,3 @@ ## 2025-02-12 - R 언어에서 반복적인 mirt 모델 생성 시 불필요한 데이터프레임 부분집합 추출 최적화 **Learning:** R에서 데이터프레임의 특정 열을 추출하는 작업(`df[cols]`)은 O(N)의 메모리 복사를 수반합니다. `autoFIPC`에서 `mirt` 모델의 파라미터를 설정하거나 호출하는 과정 중에 `newformXDataK[colnames(newFormModel@Data$data)]` 코드가 반복해서 사용되었고, 심지어 `ncol()`을 위해 단순히 개수를 구할 때도 사용되어 불필요한 메모리 할당과 오버헤드를 초래했습니다. **Action:** 조건문이나 반복문 내부에서 불필요하게 데이터프레임 부분집합 연산이 반복되지 않도록 외부에서 한 번만 `linkedFormData <- newformXDataK[colnames(newFormModel@Data$data)]`로 캐싱(caching)한 뒤, `ncol(linkedFormData)`와 `data = linkedFormData` 형태로 재사용하여 메모리 복사와 O(N) 오버헤드를 방지해야 합니다. -## 2024-09-12 - [na.omit() 대신 logical indexing을 활용한 고유값 계산 최적화] -**Learning:** R에서 `length(unique(stats::na.omit(x)))` 대신 `sum(!is.na(unique(x)))`를 사용하면 메서드 디스패치와 추가 메모리 할당(na.action) 오버헤드를 줄여 실행 성능을 높일 수 있다. -**Action:** 고유값 개수를 샐 때 NA를 제외하는 경우 항상 논리 인덱싱의 합계를 사용하는 패턴을 적용해야 한다.