phop 의 foreach 문 제 를 깊이 분석 하 다.
$arr = array(1,2,3);
foreach($arr as $k => &$v) {
$v = $v * 2;
}
// now $arr is array(2, 4, 6)
foreach($arr as $k => $v) {
echo "$k", " => ", "$v";
}
먼저 간단 한 것 부터 시작 합 니 다.만약 에 우리 가 상기 코드 를 실행 하려 고 시도 하면 마지막 출력 이 0=>2 인 것 을 발견 할 수 있 습 니 다. 1=>4 2=>4 。왜 0=>2 가 아 닙 니까? 1=>4 2=>6 ?사실,우 리 는 foreach($arr as$k=>$v)구조 가 다음 과 같은 조작 을 포함 하고 있다 고 볼 수 있 습 니 다.각각 배열 의 현재'키'와 현재'값'을 변수$k 와$v 에 부여 합 니 다.구체 적 인 전개 형 예 를 들 어
foreach($arr as $k => $v){
// 2
$v = currentVal();
$k = currentKey();
//
……
}
상기 이론 에 따 르 면 지금 우 리 는 다음 의 foreach:첫 번 째 순환,$v 는 하나의 인용 이기 때문에$v=&$arr[0],$v=$v*2 는$arr[0]*2 에 해당 하기 때문에$arr 은 2,2,3 번 째 순환,$v=&$arr[1],$arr 은 2,4,3 번 째 순환,$v=&$arr[2],$arr 은 2,4 로 변 한다.6.그 후에 코드 는 두 번 째 foreach 에 들 어 갔다.첫 번 째 순환 은$v=$arr[0]을 포함 하여 촉발 되 었 다.이때$v 는$arr[2]의 인용,즉$arr[2]=$arr[0],$arr 은 2,4,2 번 째 순환 이 되 었 고$v=$arr[1],즉$arr[2]=$arr[1],$arr 는 2,4,4 번 째 순환 이 되 었 으 며$v=$arr[2],즉$arr[2],$arr 는 2,4,4 OK 가 되 어 분석 이 완료 되 었 다.어떻게 비슷 한 문 제 를 해결 합 니까?php 매 뉴 얼 에 있 는 알림:Warning:배열 의 마지막 요소 인$value 는 foreach 순환 후에 도 유 지 됩 니 다.unset()를 사용 하여 소각 하 는 것 을 권장 합 니 다.
$arr = array(1,2,3);
foreach($arr as $k => &$v) {
$v = $v * 2;
}
unset($v);
foreach($arr as $k => $v) {
echo "$k", " => ", "$v";
}
// 0=>2 1=>4 2=>6
은 이 문제 에서 인용 이 부작용 을 동반 할 가능성 이 높다 는 것 을 알 수 있다.무의식 적 인 수정 으로 인해 배열 의 내용 이 변경 되 지 않 으 려 면 이 인용 을 제때에 취소 하 는 것 이 좋다.문제 2:
$arr = array('a','b','c');
foreach($arr as $k => $v) {
echo key($arr), "=>", current($arr);
}
// 1=>b 1=>b 1=>b
이 문 제 는 더욱 기괴 하 다.매 뉴 얼 에 따 르 면 키 와 current 는 각각 배열 의 현재 요 소 를 가 져 오 는 키 입 니 다.그런데 왜 키($arr)는 항상 1,current($arr)는 항상 b 일 까요?컴 파일 된 opcode: 을 vld 로 먼저 봅 니 다.세 번 째 줄 의 ASSIGN 명령 을 보면 array('a','b','c')를$arr 에 할당 하 는 것 을 의미 합 니 다.$arr 는 CV 이 고 array('a','b','c')는 TMP 이기 때문에 ASSIGN 명령 에서 실제 실 행 된 함 수 를 찾 으 면 ZEND 입 니 다.ASSIGN_SPEC_CV_TMP_HANDLER。여기 서 특별히 지적 해 야 할 것 은 CV 는 PHP 5.1 이후 에 야 추 가 된 변수 cache 입 니 다.이것 은 zval**를 배열 형식 으로 저장 합 니 다.cache 에 사 는 변 수 를 다시 사용 할 때 active 기호 표를 찾 지 않 고 CV 배열 에서 직접 가 져 옵 니 다.배열 의 접근 속도 가 hash 표를 훨씬 초과 하기 때문에 효율 을 높 일 수 있 습 니 다.
static int ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zend_free_op free_op2;
zval *value = _get_zval_ptr_tmp(&opline->op2, EX(Ts), &free_op2 TSRMLS_CC);
// CV $arr**
zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
if (IS_CV == IS_VAR && !variable_ptr_ptr) {
……
}
else {
// array $arr
value = zend_assign_to_variable(variable_ptr_ptr, value, 1 TSRMLS_CC);
if (!RETURN_VALUE_UNUSED(&opline->result)) {
AI_SET_PTR(EX_T(opline->result.u.var).var, value);
PZVAL_LOCK(value);
}
}
ZEND_VM_NEXT_OPCODE();
}
ASSIGN 명령 이 완 료 된 후에 CV 배열 에 zval**지침 이 추가 되 었 고 포인터 가 실제 array 를 가리 키 는 것 은$arr 가 CV 캐 시 되 었 음 을 나타 낸다. 다음 배열 의 순환 작업 을 수행 합 니 다.FE 를 보 겠 습 니 다.RESET 명령 에 대응 하 는 실행 함 수 는 ZEND 입 니 다.FE_RESET_SPEC_CV_HANDLER:
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
if (……) {
……
} else {
// CV array
array_ptr = _get_zval_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
……
}
……
// array zend_execute_data->Ts (Ts temp_variable)
AI_SET_PTR(EX_T(opline->result.u.var).var, array_ptr);
PZVAL_LOCK(array_ptr);
if (iter) {
……
} else if ((fe_ht = HASH_OF(array_ptr)) != NULL) {
//
zend_hash_internal_pointer_reset(fe_ht);
if (ce) {
……
}
is_empty = zend_hash_has_more_elements(fe_ht) != SUCCESS;
// EX_T(opline->result.u.var).fe.fe_pos
zend_hash_get_pointer(fe_ht, &EX_T(opline->result.u.var).fe.fe_pos);
} else {
……
}
……
}
여 기 는 주로 중요 한 지침 2 개 를 zend 에 저장 합 니 다.execute_data->Ts 중:•EXT(opline->result.u.var).var----array 를 가리 키 는 지침•EXT(opline->result.u.var).fe.fe_pos---array 내부 요 소 를 가리 키 는 포인터 FERESET 명령 이 실 행 된 후 메모리 의 실제 상황 은 다음 과 같 습 니 다.이어서 FE 를 계속 살 펴 보 겠 습 니 다.FETCH,이에 대응 하 는 실행 함 수 는 ZEND 입 니 다.FE_FETCH_SPEC_VAR_HANDLER:
static int ZEND_FASTCALL ZEND_FE_FETCH_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
// EX_T(opline->op1.u.var).var.ptr
zval *array = EX_T(opline->op1.u.var).var.ptr;
……
switch (zend_iterator_unwrap(array, &iter TSRMLS_CC)) {
default:
case ZEND_ITER_INVALID:
……
case ZEND_ITER_PLAIN_OBJECT: {
……
}
case ZEND_ITER_PLAIN_ARRAY:
fe_ht = HASH_OF(array);
// :
// FE_RESET EX_T(opline->op1.u.var).fe.fe_pos
//
zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
//
if (zend_hash_get_current_data(fe_ht, (void **) &value)==FAILURE) {
ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_num);
}
if (use_key) {
key_type = zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len, &int_key, 1, NULL);
}
//
zend_hash_move_forward(fe_ht);
// EX_T(opline->op1.u.var).fe.fe_pos
zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
break;
case ZEND_ITER_OBJECT:
……
}
……
}
근거 FEFETCH 의 실현 은 foreach($arr as$k=>$v)가 하 는 일 을 대체적으로 알 게 되 었 습 니 다.zendexecute_data->Ts 의 지침 은 배열 요 소 를 가 져 오고 성공 한 후에 이 지침 을 다음 위치 로 이동 하여 다시 저장 합 니 다.쉽게 말 하면 첫 번 째 순환 중 FEFETCH 에서 배열 의 내부 지침 을 두 번 째 요소 로 이동 시 켰 기 때문에 foreach 내부 에서 key($arr)와 current($arr)를 호출 할 때 실제 적 으로 얻 은 것 은 1 과'b'입 니 다.그런데 왜 1=>b 를 세 번 출력 합 니까?9 번 줄 과 13 번 줄 의 SEND 를 계속 보 겠 습 니 다.REF 명령 은$arr 인 자 를 스 택 에 저장 하 는 것 을 표시 합 니 다.이어서 보통 DO 를 사용 합 니 다.FCALL 명령 은 key 와 current 함 수 를 호출 합 니 다.PHP 는 로 컬 기기 코드 로 컴 파일 되 지 않 았 기 때문에 php 는 이러한 opcode 명령 을 사용 하여 실제 CPU 와 메모리 의 작업 방식 을 모 의 합 니 다.PHP 원본 코드 의 SEND 찾기REF:
static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
// CV $arr
varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
……
// , copy array key
SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
varptr = *varptr_ptr;
Z_ADDREF_P(varptr);
//
zend_vm_stack_push(varptr TSRMLS_CC);
ZEND_VM_NEXT_OPCODE();
}
상기 코드 중의 SEPARATEZVAL_TO_MAKE_IS_REF 는 매크로:
#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv) \
if (!PZVAL_IS_REF(*ppzv)) { \
SEPARATE_ZVAL(ppzv); \
Z_SET_ISREF_PP((ppzv)); \
}
SEPARATEZVAL_TO_MAKE_IS_REF 의 주요 역할 은 변수 가 인용 이 아니라면 메모리 에 새 것 을 복사 하 는 것 이다.이 예 에서 array('a','b','c')를 복사 하 였 습 니 다.따라서 변 수 를 분리 한 후의 메모 리 는 입 니 다.변 수 를 분리 한 후에 CV 배열 의 지침 은 새로운 copy 를 통 해 나 온 데 이 터 를 가리 키 고 zendexecute_data->Ts 의 지침 은 오래된 데 이 터 를 얻 을 수 있 습 니 다.다음 순환 은 일일이 설명 하지 않 겠 습 니 다.위의 그림 과 결합 하면 foreach 구 조 는 아래 파란색 array 를 사용 합 니 다.a,b,c·key,current 는 위 노란색 array 를 사용 합 니 다.내부 지침 은 b 를 영원히 가리 키 고 있 습 니 다.우 리 는 왜 key 와 current 가 array 의 두 번 째 요 소 를 되 돌려 주 는 지 알 게 되 었 습 니 다.외부 코드 가 copy 에 작용 하 는 array 가 없 기 때 문 입 니 다.그것 의 내부 지침 은 영원히 움 직 이지 않 을 것 이다.문제 3:
$arr = array('a','b','c');
foreach($arr as $k => &$v) {
echo key($arr), '=>', current($arr);
}// 1=>b 2=>c =>
본 문제 와 문제 2 는 약간의 차이 만 있다.본 문제 의 foreach 는 인용 을 사용 했다.VLD 로 본 문 제 를 살 펴 보 니 문제 2 코드 로 컴 파일 된 opcode 와 같 습 니 다.따라서 우 리 는 문제 2 의 추적 방법 을 이용 하여 opcode 에 대응 하 는 실현 을 점차적으로 살 펴 본다.우선 foreach 는 FE 를 호출 합 니 다.RESET:
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
// CV
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
……
}
else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
……
}
else {
// array
if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
if (opline->extended_value & ZEND_FE_FETCH_BYREF) {
// array zval is_ref
Z_SET_ISREF_PP(array_ptr_ptr);
}
}
array_ptr = *array_ptr_ptr;
Z_ADDREF_P(array_ptr);
}
} else {
……
}
……
}
문제 2 에서 FE 일 부 를 분 석 했 습 니 다.RESET 의 실현.특별한 주의 가 필요 합 니 다.이 foreach 획득 값 은 인용 을 사 용 했 기 때문에 실행 할 때 FERESET 에 서 는 이전 문제 와 다른 다른 지점 으로 들 어 갑 니 다.결국,FERESET 는 array 의 is 를ref 는 true 로 설정 되 어 있 습 니 다.이 때 메모리 에 array 의 데이터 만 있 습 니 다.다음은 SEND 분석REF:
static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
// CV $arr
varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
……
// , CV , copy array
SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
varptr = *varptr_ptr;
Z_ADDREF_P(varptr);
//
zend_vm_stack_push(varptr TSRMLS_CC);
ZEND_VM_NEXT_OPCODE();
}
매크로 SEPARATEZVAL_TO_MAKE_IS_REF 분리 isref=false 의 변수.이전에 array 가 is 설정 되 었 기 때문에ref=true,따라서 복사 본 은 복사 되 지 않 습 니 다.메모리 에 array 데이터 가 하나 밖 에 없다 는 얘 기다.위의 그림 은 앞의 2 차 순환 이 왜 1=>b 2=>C 를 출력 하 는 지 설명 한다.3 차 순환 FEFETCH 때 는 포인 터 를 계속 앞으로 이동 합 니 다.
ZEND_API int zend_hash_move_forward_ex(HashTable *ht, HashPosition *pos)
{
HashPosition *current = pos ? pos : &ht->pInternalPointer;
IS_CONSISTENT(ht);
if (*current) {
*current = (*current)->pListNext;
return SUCCESS;
} else
return FAILURE;
}
이때 내부 포인터 가 배열 의 마지막 요 소 를 가리 키 기 때문에 앞으로 이동 하면 NULL 을 가리 킬 것 입 니 다.내부 포인 터 를 NULL 에 가리 키 면 배열 에 key 와 current 를 호출 하면 각각 NULL 과 false 로 돌아 가 호출 에 실 패 했 음 을 표시 합 니 다.이 때 echo 에서 문 자 를 내지 않 습 니 다. 문제 4:
$arr = array(1, 2, 3);
$tmp = $arr;
foreach($tmp as $k => &$v){
$v *= 2;
}
var_dump($arr, $tmp); // ?
이 문 제 는 foreach 와 관계 가 크 지 않 지만 foreach 와 관련 된 이상 함께 토론 합 시다.)코드 에 먼저 배열$arr 를 만 든 다음 에 이 배열 을$tmp 에 부 여 했 습 니 다.다음 foreach 순환 에서$v 를 수정 하면 배열$tmp 에 작용 하지만$arr 에는 작용 하지 않 습 니 다.왜 일 까요?이것 은 php 에서 할당 연산 은 한 변수의 값 을 다른 변수 에 복사 하기 때문에 그 중 하 나 를 수정 하면 다른 변수 에 영향 을 주지 않 습 니 다.주제:이것 은 object 형식 에 적용 되 지 않 습 니 다.PHP 5 부터 대상 의 기본 값 은 인용 을 통 해 할당 합 니 다.예 를 들 어
class A{
public $foo = 1;
}
$a1 = $a2 = new A;
$a1->foo=100;
echo $a2->foo; // 100,$a1 $a2
에서 문제 의 코드 로 돌아 갑 니 다.지금 우 리 는$tmp=$arr 가 사실은 값 복사 라 는 것 을 확인 할 수 있 습 니 다.전체$arr 배열 은$tmp 에 다시 복 사 됩 니 다.이론 적 으로 할당 문 구 를 실행 한 후에 메모리 에 똑 같은 배열 이 2 개 있 을 것 이다.만약 수조 가 매우 크다 면 이런 조작 이 매우 느 리 지 않 겠 느 냐 는 동창 회 의 의문 이 있 을 지도 모른다.다행히 phop 은 더 똑똑 한 처리 방법 이 있 습 니 다.실제로$tmp=$arr 가 실 행 된 후에 도 메모리 에는 array 가 하나 밖 에 없습니다.php 소스 코드 의 zend 보기assign_to_variable 구현(phop 5.3.26 에서 따 온 것):
static inline zval* zend_assign_to_variable(zval **variable_ptr_ptr, zval *value, int is_tmp_var TSRMLS_DC)
{
zval *variable_ptr = *variable_ptr_ptr;
zval garbage;
……
// object
if (Z_TYPE_P(variable_ptr) == IS_OBJECT && Z_OBJ_HANDLER_P(variable_ptr, set)) {
……
}
//
if (PZVAL_IS_REF(variable_ptr)) {
……
} else {
// refcount__gc=1
if (Z_DELREF_P(variable_ptr)==0) {
……
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(*variable_ptr_ptr);
//
if (!is_tmp_var) {
if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {
ALLOC_ZVAL(variable_ptr);
*variable_ptr_ptr = variable_ptr;
*variable_ptr = *value;
Z_SET_REFCOUNT_P(variable_ptr, 1);
zval_copy_ctor(variable_ptr);
} else {
// $tmp=$arr ,
// value $arr array ,variable_ptr_ptr $tmp
// ,
*variable_ptr_ptr = value;
// value refcount__gc +1, refcount__gc 1,Z_ADDREF_P 2
Z_ADDREF_P(value);
}
} else {
……
}
}
Z_UNSET_ISREF_PP(variable_ptr_ptr);
}
return *variable_ptr_ptr;
}
에서$tmp=$arr 의 본질은 array 의 지침 을 복사 한 다음 array 의 refcount 를 자동 으로 1.그림 으로 표현 하 는 것 입 니 다.여전히 하나의 array 배열 만 있 습 니 다. array 만 있 는 이상 foreach 순환 에서$tmp 를 수정 할 때 왜$arr 는 변 하지 않 았 습 니까?PHP 소스 의 ZEND 계속 보기FE_RESET_SPEC_CV_HANDLER 함수,이것 은 OPCODE HANDLER 입 니 다.이에 대응 하 는 OPCODE 는 FE 입 니 다.RESET。이 함 수 는 foreach 가 시작 되 기 전에 배열 의 내부 지침 을 첫 번 째 요 소 를 가리 키 는 것 을 책임 집 니 다.
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zval *array_ptr, **array_ptr_ptr;
HashTable *fe_ht;
zend_object_iterator *iter = NULL;
zend_class_entry *ce = NULL;
zend_bool is_empty = 0;
// FE_RESET
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
……
}
// foreach object
else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
……
}
else {
//
if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
// SEPARATE_ZVAL_IF_NOT_REF
//
// $tmp $arr, 2
SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
if (opline->extended_value & ZEND_FE_FETCH_BYREF) {
Z_SET_ISREF_PP(array_ptr_ptr);
}
}
array_ptr = *array_ptr_ptr;
Z_ADDREF_P(array_ptr);
}
} else {
……
}
//
……
}
코드 에서 알 수 있 듯 이 실제 실행 변수 분 리 는 할당 문 구 를 실행 할 때 가 아니 라 변 수 를 사용 할 때 로 미 루 었 다.이것 도 Copy On Write 체제 가 PHP 에서 이 루어 진 것 이다.FE_RESET 이후 메모리 의 변 화 는 다음 과 같다. 위의 그림 은 왜 foreach 가 원래 의$arr 에 영향 을 주지 않 는 지 설명 했다.에 대하 여 refcount 및 isref 의 변화 상황,관심 있 는 학생 은 ZEND 를 자세히 읽 을 수 있 습 니 다.FE_RESET_SPEC_CV_HANDLER 와 ZENDSWITCH_FREE_SPEC_VAR_HANDLER 의 구체 적 인 실현(모두 php-src/zend/zend 에 있 음vm_execute.h 중)본 고 는 상세 한 분석 을 하지 않 는 다.)
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Laravel - 변환된 유효성 검사 규칙으로 API 요청 제공동적 콘텐츠를 위해 API를 통해 Laravel CMS에 연결하는 모바일 앱(또는 웹사이트) 구축을 고려하십시오. 이제 앱은 CMS에서 번역된 콘텐츠를 받을 것으로 예상되는 다국어 앱이 될 수 있습니다. 일반적으로 ...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.